mirror of
https://github.com/nuxt/nuxt.git
synced 2025-02-07 17:32:31 +00:00
Prototype 0.1.0 working
Alpha 0.1.0
This commit is contained in:
parent
6cce8b161a
commit
8ab135af55
27
.eslintrc.js
Normal file
27
.eslintrc.js
Normal 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
3
.gitignore
vendored
@ -1,6 +1,3 @@
|
|||||||
# build output
|
|
||||||
dist
|
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
yarn.lock
|
yarn.lock
|
||||||
node_modules
|
node_modules
|
||||||
|
@ -47,5 +47,6 @@ So far, we get:
|
|||||||
- 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 `/static/`
|
||||||
|
- Config file nuxt.config.js
|
||||||
|
|
||||||
To see how simple this is, check out the [sample app - nuxtgram](https://github.com/atinux/nuxtgram)
|
To see how simple this is, check out the [sample app - nuxtgram](https://github.com/atinux/nuxtgram)
|
||||||
|
29
bin/nuxt
Executable file
29
bin/nuxt
Executable 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
81
bin/nuxt-init
Executable 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
70
bin/nuxt-start
Executable 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
18
examples/basic-css/pages/index.vue
Executable file
18
examples/basic-css/pages/index.vue
Executable 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>
|
15
examples/head-elements/pages/index.vue
Executable file
15
examples/head-elements/pages/index.vue
Executable 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>
|
3
examples/hello-world/pages/about.vue
Executable file
3
examples/hello-world/pages/about.vue
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<div>About us</div>
|
||||||
|
</template>
|
3
examples/hello-world/pages/index.vue
Executable file
3
examples/hello-world/pages/index.vue
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<div>Hello World. <router-link to='/about'>About</router-link></div>
|
||||||
|
</template>
|
12
examples/nested-components/components/paragraph.vue
Executable file
12
examples/nested-components/components/paragraph.vue
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<p class="p">
|
||||||
|
<slot></slot>
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.p {
|
||||||
|
font: 13px Helvetica, Arial;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
</style>
|
25
examples/nested-components/components/post.vue
Executable file
25
examples/nested-components/components/post.vue
Executable 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>
|
54
examples/nested-components/pages/index.vue
Executable file
54
examples/nested-components/pages/index.vue
Executable 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
25
examples/with-ava/Readme.md
Executable 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
11
examples/with-ava/package.json
Executable file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "ava-tests",
|
||||||
|
"scripts": {
|
||||||
|
"start": "../../bin/nuxt .",
|
||||||
|
"test": "ava"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"ava": "^0.16.0",
|
||||||
|
"jsdom": "^9.8.3"
|
||||||
|
}
|
||||||
|
}
|
19
examples/with-ava/pages/index.vue
Executable file
19
examples/with-ava/pages/index.vue
Executable 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>
|
77
examples/with-ava/test/index.test.js
Executable file
77
examples/with-ava/test/index.test.js
Executable 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
8
index.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/*!
|
||||||
|
* nuxt.js
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
module.exports = require('./lib/nuxt')
|
40
lib/app/App.vue
Normal file
40
lib/app/App.vue
Normal 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
179
lib/app/client.js
Normal 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)
|
||||||
|
})
|
31
lib/app/components/Loading.vue
Normal file
31
lib/app/components/Loading.vue
Normal 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
24
lib/app/index.js
Normal 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
38
lib/app/router.js
Normal 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
70
lib/app/server.js
Normal 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
38
lib/app/utils.js
Normal 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
248
lib/build/index.js
Normal 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
|
||||||
|
}
|
52
lib/build/webpack/base.config.js
Normal file
52
lib/build/webpack/base.config.js
Normal 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]'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
55
lib/build/webpack/client.config.js
Normal file
55
lib/build/webpack/client.config.js
Normal 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
|
23
lib/build/webpack/server.config.js
Normal file
23
lib/build/webpack/server.config.js
Normal 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
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|
16
lib/build/webpack/vue-loader.config.js
Normal file
16
lib/build/webpack/vue-loader.config.js
Normal 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
155
lib/nuxt.js
Normal 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
19
lib/render.js
Normal 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
7
lib/renderRoute.js
Normal 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
29
lib/utils.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
'use strict'
|
||||||
|
|
||||||
|
exports.encodeHtml = (str) => str.replace(/</g, '<').replace(/>/g, '>')
|
||||||
|
|
||||||
|
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
19
lib/views/app.html
Normal 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
11
lib/views/error.html
Normal 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>
|
112
package.json
112
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nuxt",
|
"name": "nuxt",
|
||||||
"version": "0.0.1",
|
"version": "0.1.0",
|
||||||
"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": "./dist/lib/index.js",
|
"main": "./dist/lib/index.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -9,76 +9,58 @@
|
|||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"bin": {
|
"bin": {
|
||||||
"next": "./dist/bin/nuxt"
|
"nuxt": "./dist/bin/nuxt"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "gulp",
|
"start": "DEBUG=nuxt:* bin/nuxt",
|
||||||
"test": "standard && gulp test",
|
"dev": "DEBUG=nuxt:* nodemon bin/nuxt",
|
||||||
"lint": "standard",
|
"test": "npm run lint",
|
||||||
"prepublish": "gulp release",
|
"lint": "eslint --ext .js,.vue bin lib pages test index.js --ignore-pattern lib/app",
|
||||||
"precommit": "npm run lint"
|
"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": {
|
"dependencies": {
|
||||||
"babel-core": "6.17.0",
|
"ansi-html": "^0.0.6",
|
||||||
"babel-generator": "6.17.0",
|
"autoprefixer": "^6.5.1",
|
||||||
"babel-loader": "6.2.5",
|
"babel-core": "^6.18.2",
|
||||||
"babel-plugin-module-alias": "1.6.0",
|
"babel-loader": "^6.2.7",
|
||||||
"babel-plugin-transform-async-to-generator": "6.16.0",
|
"babel-preset-es2015": "^6.18.0",
|
||||||
"babel-plugin-transform-class-properties": "6.16.0",
|
"babel-preset-stage-2": "^6.18.0",
|
||||||
"babel-plugin-transform-object-rest-spread": "6.16.0",
|
"co": "^4.6.0",
|
||||||
"babel-plugin-transform-runtime": "6.15.0",
|
"cross-spawn": "^5.0.1",
|
||||||
"babel-preset-es2015": "6.16.0",
|
"css-loader": "^0.25.0",
|
||||||
"babel-preset-react": "6.16.0",
|
"debug": "^2.2.0",
|
||||||
"babel-runtime": "6.11.6",
|
"del": "^2.2.2",
|
||||||
"cross-spawn": "4.0.2",
|
"es6-object-assign": "^1.0.3",
|
||||||
"del": "2.2.2",
|
"es6-promise": "^4.0.5",
|
||||||
"glamor": "2.17.10",
|
"extract-text-webpack-plugin": "2.0.0-beta.4",
|
||||||
"glob-promise": "1.0.6",
|
"file-loader": "^0.9.0",
|
||||||
"htmlescape": "1.1.1",
|
"glob-promise": "^1.0.6",
|
||||||
"loader-utils": "0.2.16",
|
"hash-sum": "^1.0.2",
|
||||||
"minimist": "1.2.0",
|
"lodash": "^4.16.6",
|
||||||
"mz": "2.4.0",
|
"lru-cache": "^4.0.1",
|
||||||
"path-match": "1.2.4",
|
"mkdirp-then": "^1.2.0",
|
||||||
"react": "15.3.2",
|
"pify": "^2.3.0",
|
||||||
"react-dom": "15.3.2",
|
"serialize-javascript": "^1.3.0",
|
||||||
"react-hot-loader": "3.0.0-beta.6",
|
"serve-static": "^1.11.1",
|
||||||
"send": "0.14.1",
|
"url-loader": "^0.5.7",
|
||||||
"strip-ansi": "3.0.1",
|
"vue": "^2.0.5",
|
||||||
"url": "0.11.0",
|
"vue-loader": "^9.8.0",
|
||||||
"webpack": "1.13.2",
|
"vue-meta": "^0.0.0",
|
||||||
"webpack-dev-server": "1.16.2",
|
"vue-router": "^2.0.1",
|
||||||
"write-file-webpack-plugin": "3.3.0"
|
"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": {
|
"devDependencies": {
|
||||||
"babel-eslint": "7.0.0",
|
"babel-eslint": "^7.1.0",
|
||||||
"babel-plugin-transform-remove-strict-mode": "0.0.2",
|
"benchmark": "^2.1.2",
|
||||||
"benchmark": "2.1.1",
|
"eslint": "^3.8.1",
|
||||||
"gulp": "3.9.1",
|
"eslint-config-standard": "^6.2.1",
|
||||||
"gulp-ava": "0.14.1",
|
"eslint-plugin-html": "^1.5.5",
|
||||||
"gulp-babel": "6.1.2",
|
"eslint-plugin-promise": "^3.3.0",
|
||||||
"gulp-benchmark": "1.1.1",
|
"eslint-plugin-standard": "^2.0.1",
|
||||||
"gulp-cached": "1.1.0",
|
"nodemon": "^1.11.0"
|
||||||
"gulp-notify": "2.2.0",
|
|
||||||
"husky": "0.11.9",
|
|
||||||
"run-sequence": "1.2.2",
|
|
||||||
"standard": "^8.4.0",
|
|
||||||
"webpack-stream": "3.2.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
61
pages/_error-debug.vue
Executable file
61
pages/_error-debug.vue
Executable 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
60
pages/_error.vue
Normal 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
13
test/fixtures/basic/pages/async-props.vue
vendored
Executable 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
9
test/fixtures/basic/pages/css.vue
vendored
Executable 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
8
test/fixtures/basic/pages/head.vue
vendored
Executable 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
16
test/fixtures/basic/pages/stateful.vue
vendored
Executable 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
3
test/fixtures/basic/pages/stateless.vue
vendored
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<h1>My component!</h1>
|
||||||
|
</template>
|
39
test/index.js
Executable file
39
test/index.js
Executable 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 })
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user