Merge branch 'nested-dynamic-routes'

This commit is contained in:
Sébastien Chopin 2016-12-19 20:37:24 +01:00
commit 004e21b929
46 changed files with 726 additions and 145 deletions

13
.editorconfig Normal file
View File

@ -0,0 +1,13 @@
# editorconfig.org
root = true
[*]
indent_size = 2
indent_style = space
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false

View File

@ -4,6 +4,8 @@ node_js:
- "6.9"
- "5.12"
- "4.7"
before_install:
- if [[ `npm -v` != 3* ]]; then npm i -g npm@3; fi
install:
- npm install
- npm run build

View File

@ -5,6 +5,7 @@ process.env.DEBUG = 'nuxt:*'
var _ = require('lodash')
var debug = require('debug')('nuxt:build')
debug.color = 2 // force green color
var fs = require('fs')
var Nuxt = require('../')
var chokidar = require('chokidar')
@ -37,7 +38,7 @@ nuxt.build()
function listenOnConfigChanges (nuxt, server) {
// Listen on nuxt.config.js changes
var build = _.debounce(() => {
debug('[nuxt.config.js] changed, rebuilding the app...')
debug('[nuxt.config.js] changed')
delete require.cache[nuxtConfigFile]
var options = {}
if (fs.existsSync(nuxtConfigFile)) {
@ -50,6 +51,8 @@ function listenOnConfigChanges (nuxt, server) {
options.rootDir = rootDir
nuxt.close()
.then(() => {
nuxt.renderer = null
debug('Rebuilding the app...')
return new Nuxt(options).build()
})
.then((nuxt) => {

View File

@ -2,7 +2,7 @@
<template>
<div>
<p>{{ userAgent }}</p>
<p><router-link to="/post">See a post (http request / Ajax)</router-link></p>
<p><nuxt-link to="/post">See a post (http request / Ajax)</nuxt-link></p>
</div>
</template>

View File

@ -2,7 +2,7 @@
<template>
<div>
<p>{{ post.title }}!</p>
<p><router-link to="/">Back home</router-link></p>
<p><nuxt-link to="/">Back home</nuxt-link></p>
</div>
</template>

View File

@ -183,7 +183,7 @@ Let's add a `/secret` route where only the connected user can see its content:
<template>
<div>
<h1>Super secret page</h1>
<router-link to="/">Back to the home page</router-link>
<nuxt-link to="/">Back to the home page</nuxt-link>
</div>
</template>

View File

@ -14,7 +14,7 @@
<p><i>You can also refresh this page, you'll still be connected!</i></p>
<button @click="logout">Logout</button>
</div>
<p><router-link to="/secret">Super secret page</router-link></p>
<p><nuxt-link to="/secret">Super secret page</nuxt-link></p>
</div>
</template>

View File

@ -2,7 +2,7 @@
<div>
<h1>Super secret page</h1>
<p>If you try to access this URL not connected, you will be redirected to the home page (server-side or client-side)</p>
<router-link to="/">Back to the home page</router-link>
<nuxt-link to="/">Back to the home page</nuxt-link>
</div>
</template>

View File

@ -1,7 +1,7 @@
<template>
<div class="container">
<p>About Page</p>
<router-link to="/">Go to /</router-link>
<nuxt-link to="/">Go to /</nuxt-link>
</div>
</template>

View File

@ -1,7 +1,7 @@
<template>
<div class="container">
<p>Hello {{ name }}!</p>
<router-link to="/about">Go to /about</router-link>
<nuxt-link to="/about">Go to /about</nuxt-link>
</div>
</template>

View File

@ -2,38 +2,177 @@
> Nuxt.js is based on vue-router and allows you to defined custom routes :rocket:
## Usage
## Concept
Nuxt.js detect and generate automatically the vue-router config according to your file tree of .vue files inside the `pages` directory.
## Basic routes
This file tree:
```bash
/pages
|-> /team
|-> index.vue
|-> about.vue
|-> index.vue
```
will automatically generate:
Add your custom routes inside `nuxt.config.js`:
```js
module.exports = {
router: {
routes: [
{ name: 'user', path: '/users/:id', component: 'pages/user' }
]
{
name: 'index',
path: '/',
component: 'pages/index'
},
{
name: 'team',
path: '/team',
component: 'pages/team/index'
},
{
name: 'team-about',
path: '/team/about',
component: 'pages/team/about'
}
]
}
```
| key | Optional? | definition |
|------|------------|-----------|
| `path` | **Required** | Route path, it can have dynamic mapping, look at [vue-router documentation](https://router.vuejs.org/en/essentials/dynamic-matching.html) about it. |
| `component` | **Required** | Path to the `.vue` component, if relative, it has to be from the app folder. |
| `name` | Optional | Route name, useful for linking to it with `<router-link>`, see [vue-router documentation](https://router.vuejs.org/en/essentials/named-routes.html) about it. |
| `meta` | Optional | Let you add custom fields to get back inside your component (available in the context via `route.meta` inside `data` and `fetch` methods). See [vue-router documentation](https://router.vuejs.org/en/advanced/meta.html) about it. |
| `children` | Optional | *Not supported* |
## Dynamic routes
## Hidden pages
To define a dynamic route with a param, you need to define a .vue file prefixed by an underscore.
>If you want don't want nuxt.js to generate a route for a specific page, you just have to **rename it with _ at the beginning**.
This file tree:
Let's say I have a component `pages/user.vue` and I don't want nuxt.js to create the `/user`. I can rename it to `pages/_user.vue` and voilà!
```bash
/pages
|-> /projects
|-> index.vue
|-> _slug.vue
```
will automatically generate:
You can then change the component path in the `nuxt.config.js`:
```js
// ...
{ name: 'user', path: '/users/:id', component: 'pages/_user' }
// ...
router: {
routes: [
{
name: 'projects',
path: '/projects',
component: 'pages/projects/index'
},
{
name: 'projects-slug',
path: '/projects/:slug',
component: 'pages/projects/_slug'
}
]
}
```
### Additional feature : validate (optional)
Nuxt.js allows you to define a validator function inside your dynamic route component (In this example: `pages/projects/_slug.vue`).
If validate function fails, Nuxt.js will automatically load the 404 error page.
```js
<script>
export default {
validate ({ params }) {
return /^[A-z]+$/.test(params.slug)
}
}
</script>
```
## Nested Routes (children)
To define a nested route, you need to define a .vue file with the same name as the directory which contain your children views.
> Don't forget to put `<nuxt-child></nuxt-child>` inside your parent .vue file.
This file tree:
```bash
/pages
|-> /users
|-> _id.vue
|-> users.vue
```
will automatically generate:
```js
router: {
routes: [
{
path: '/users',
component: 'pages/users',
children: [
{
path: ':id',
component: 'pages/users/_id',
name: 'users-id'
}
]
}
]
}
```
## Dynamic Nested Routes
This file tree:
```bash
/pages
|-> /posts
|-> /_slug
|-> _name.vue
|-> comments.vue
|-> _slug.vue
|-> index.vue
|-> posts.vue
```
will automatically generate:
```js
router: {
routes: [
{
path: '/posts',
component: 'pages/posts',
children: [
{
path: "",
component: 'pages/posts/index',
name: 'posts'
},
{
path: ':slug',
component: 'pages/posts/_slug',
children: [
{
path: 'comments',
component: 'pages/posts/_slug/comments',
name: 'posts-slug-comments'
},
{
path: ':name',
component: 'pages/posts/_slug/_name',
name: 'posts-slug-name'
}
]
}
]
}
]
}
```
## Demo

View File

@ -1,9 +1,4 @@
module.exports = {
router: {
routes: [
{ name: 'user', path: '/users/:id(\\d+)', component: 'pages/_user' }
]
},
build: {
vendor: ['axios']
}

View File

@ -3,7 +3,7 @@
<h2>Users</h2>
<ul class="users">
<li v-for="user in users">
<router-link :to="{ name: 'user', params: { id: user.id } }">{{ user.name }}</router-link>
<nuxt-link :to="'/users/'+user.id">{{ user.name }}</nuxt-link>
</li>
</ul>
</div>

View File

@ -3,7 +3,7 @@
<h3>{{ name }}</h3>
<h4>@{{ username }}</h4>
<p>Email : {{ email }}</p>
<p><router-link to="/">List of users</router-link></p>
<p><nuxt-link to="/">List of users</nuxt-link></p>
</div>
</template>
@ -11,8 +11,11 @@
import axios from 'axios'
export default {
validate ({ params }) {
return !isNaN(+params.id)
},
data ({ params, error }) {
return axios.get(`https://jsonplaceholder.typicode.com/users/${params.id}`)
return axios.get(`https://jsonplaceholder.typicode.com/users/${+params.id}`)
.then((res) => res.data)
.catch(() => {
error({ message: 'User not found', statusCode: 404 })

View File

@ -1,7 +1,7 @@
<template>
<div>
<p>Hi from {{ name }}</p>
<router-link to="/">Home page</router-link>
<nuxt-link to="/">Home page</nuxt-link>
</div>
</template>

View File

@ -1,6 +1,6 @@
<template>
<div class="container">
<h1>Welcome!</h1>
<router-link to="/about">About page</router-link>
<nuxt-link to="/about">About page</nuxt-link>
</div>
</template>

View File

@ -1,7 +1,7 @@
<template>
<div class="content">
<h1 class="title">Another Page</h1>
<p><router-link to="/" class="button is-medium is-info hvr-wobble-vertical">Another button</router-link></p>
<p><router-link to="/">Back home</router-link></p>
<p><nuxt-link to="/" class="button is-medium is-info hvr-wobble-vertical">Another button</nuxt-link></p>
<p><nuxt-link to="/">Back home</nuxt-link></p>
</div>
</template>

View File

@ -1,7 +1,7 @@
<template>
<div class="content">
<h1 class="title">Custom CSS!</h1>
<p><router-link to="/about" class="button is-medium is-primary hvr-float-shadow">I am a button</router-link></p>
<p><router-link to="/about">About page</router-link></p>
<p><nuxt-link to="/about" class="button is-medium is-primary hvr-float-shadow">I am a button</nuxt-link></p>
<p><nuxt-link to="/about">About page</nuxt-link></p>
</div>
</template>

View File

@ -3,7 +3,7 @@
<h1>About page</h1>
<p>Click below to see the custom meta tags added with our custom component <code>twitter-head-card</code></p>
<twitter-head-card></twitter-head-card>
<p><router-link to="/">Home page</router-link></p>
<p><nuxt-link to="/">Home page</nuxt-link></p>
</div>
</template>

View File

@ -1,7 +1,7 @@
<template>
<div class="container">
<h1>Home page 🚀</h1>
<router-link to="/about">About page</router-link>
<nuxt-link to="/about">About page</nuxt-link>
</div>
</template>

View File

@ -1,6 +1,6 @@
<template>
<div>
<h1>Welcome!</h1>
<router-link to="/about">About page</router-link>
<nuxt-link to="/about">About page</nuxt-link>
</div>
</template>

View File

@ -1,7 +1,7 @@
<template>
<div class="container">
<img :src="thumbnailUrl" />
<p><router-link to="/">Home</router-link> - About</p>
<p><nuxt-link to="/">Home</nuxt-link> - About</p>
</div>
</template>

View File

@ -1,7 +1,7 @@
<template>
<div class="container">
<p><button @click="showLoginError">Notif me!</button></p>
<p>Home - <router-link to="/about">About</router-link></p>
<p>Home - <nuxt-link to="/about">About</nuxt-link></p>
</div>
</template>

View File

@ -71,7 +71,7 @@ To define a custom transition for a specific route, simply add the `transition`
<template>
<div class="container">
<h1>About page</h1>
<router-link to="/">Home page</router-link>
<nuxt-link to="/">Home page</nuxt-link>
</div>
</template>

View File

@ -1,7 +1,7 @@
<template>
<div class="container">
<h1>About page</h1>
<router-link to="/">Home page</router-link>
<nuxt-link to="/">Home page</nuxt-link>
</div>
</template>

View File

@ -1,6 +1,6 @@
<template>
<div class="container">
<h1>Home page</h1>
<router-link to="/about">About page</router-link>
<nuxt-link to="/about">About page</nuxt-link>
</div>
</template>

View File

@ -3,7 +3,7 @@
<img src="~static/nuxt-black.png" />
<h2>Thank you for testing nuxt.js</h2>
<p>Loaded from the {{ name }}</p>
<p><router-link to="/">Back home</router-link></p>
<p><nuxt-link to="/">Back home</nuxt-link></p>
</div>
</template>

View File

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

View File

@ -2,7 +2,7 @@
<div>
<p>
<button @click="$store.commit('increment')">{{ $store.state.counter }}</button><br>
<router-link to="/">Home</router-link>
<nuxt-link to="/">Home</nuxt-link>
</p>
</div>
</template>

View File

@ -2,7 +2,7 @@
<div>
<p>
<button @click="increment">{{ counter }}</button><br>
<router-link to="/about">About</router-link>
<nuxt-link to="/about">About</nuxt-link>
</p>
</div>
</template>

View File

@ -4,16 +4,26 @@ require('es6-object-assign').polyfill()
import 'es6-promise/auto'
import Vue from 'vue'
import { app, router<%= (store ? ', store' : '') %> } from './index'
import { getMatchedComponents, getMatchedComponentsInstances, flatMapComponents, getContext, promisify, getLocation } from './utils'
import { getMatchedComponents, getMatchedComponentsInstances, flatMapComponents, getContext, promisify, getLocation, compile } from './utils'
const noopData = () => { return {} }
const noopFetch = () => {}
let _lastPaths = []
function mapTransitions(Components, to, from) {
return Components.map((Component) => {
let transition = Component.options.transition
if (typeof transition === 'function') {
return transition(to, from)
}
return transition
})
}
function loadAsyncComponents (to, ___, 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)
if (!Component.options) {
Component = Vue.extend(Component) // fix issue #6
Component._Ctor = Component
@ -44,13 +54,12 @@ function loadAsyncComponents (to, ___, next) {
})
}
function render (to, ___, next) {
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) {
@ -67,10 +76,26 @@ function render (to, ___, next) {
}
}
})
this.setTransition(Components[0].options.transition)
this.setTransitions(mapTransitions(Components, to, from))
this.error()
let nextCalled = false
Promise.all(Components.map((Component) => {
let isValid = Components.some((Component) => {
if (typeof Component.options.validate !== 'function') return true
return Component.options.validate({
params: to.params || {},
query: to.query || {}
})
})
if (!isValid) {
this.error({ statusCode: 404, message: 'This page could not be found.', url: to.path })
return next()
}
Promise.all(Components.map((Component, i) => {
// Check if only children route changed
Component._path = compile(to.matched[i].path)(to.params)
if (Component._path === _lastPaths[i] && (i + 1) !== Components.length) {
return Promise.resolve()
}
let promises = []
const _next = function (path) {
<%= (loading ? 'this.$loading.finish && this.$loading.finish()' : '') %>
@ -78,6 +103,7 @@ function render (to, ___, next) {
next(path)
}
const context = getContext({ to<%= (store ? ', store' : '') %>, isClient: true, next: _next.bind(this), error: this.error.bind(this) })
// Validate method
if (Component._data && typeof Component._data === 'function') {
var promise = promisify(Component._data, context)
promise.then((data) => {
@ -99,6 +125,7 @@ function render (to, ___, next) {
return Promise.all(promises)
}))
.then(() => {
_lastPaths = Components.map((Component, i) => compile(to.matched[i].path)(to.params))
<%= (loading ? 'this.$loading.finish && this.$loading.finish()' : '') %>
// If not redirected
if (!nextCalled) {
@ -106,6 +133,7 @@ function render (to, ___, next) {
}
})
.catch((error) => {
_lastPaths = []
this.error(error)
next(false)
})
@ -118,12 +146,14 @@ function fixPrepatch (to, ___) {
return
}
Vue.nextTick(() => {
let RouterViewComponentFile = this.$nuxt._routerViewCache.default.__file
if (typeof this.$nuxt._routerViewCache.default === 'function') RouterViewComponentFile = this.$nuxt._routerViewCache.default.options.__file
let instances = getMatchedComponentsInstances(to)
instances.forEach((instance, i) => {
instances.forEach((instance) => {
if (!instance) return;
if (instance.constructor.options.__file === RouterViewComponentFile) {
let file = instance.$parent._routerViewCache.default.__file
if (typeof instance.$parent._routerViewCache.default === 'function') {
file = instance.$parent._routerViewCache.default.options.__file
}
if (instance.constructor.options.__file === file) {
let newData = instance.constructor.options.data()
for (let key in newData) {
Vue.set(instance.$data, key, newData[key])
@ -254,8 +284,11 @@ Promise.all(resolveComponents)
store.replaceState(NUXT.state)
}
<% } %>
_app.setTransition = _app.$options._nuxt.setTransition.bind(_app)
if (Components.length) _app.setTransition(Components[0].options.transition)
_app.setTransitions = _app.$options._nuxt.setTransitions.bind(_app)
if (Components.length) {
_app.setTransitions(mapTransitions(Components, router.currentRoute))
_lastPaths = router.currentRoute.matched.map((route) => compile(route.path)(router.currentRoute.params))
}
_app.error = _app.$options._nuxt.error.bind(_app)
_app.$loading = {} // to avoid error while _app.$nuxt does not exist
if (NUXT.error) _app.error(NUXT.error)

View File

@ -0,0 +1,60 @@
import Vue from 'vue'
const transitionsKeys = [
'name',
'mode',
'css',
'type',
'enterClass',
'leaveClass',
'enterActiveClass',
'leaveActiveClass'
]
const listenersKeys = [
'beforeEnter',
'enter',
'afterEnter',
'enterCancelled',
'beforeLeave',
'leave',
'afterLeave',
'leaveCancelled'
]
export default {
name: 'nuxt-child',
functional: true,
render (h, { parent, data }) {
data.nuxtChild = true
const transitions = parent.$nuxt.nuxt.transitions
const defaultTransition = parent.$nuxt.nuxt.defaultTransition
let depth = 0
while (parent) {
if (parent.$vnode && parent.$vnode.data.nuxtChild) {
depth++
}
parent = parent.$parent
}
data.nuxtChildDepth = depth
const transition = transitions[depth] || defaultTransition
let transitionProps = {}
transitionsKeys.forEach((key) => {
if (typeof transition[key] !== 'undefined') {
transitionProps[key] = transition[key]
}
})
let listeners = {}
listenersKeys.forEach((key) => {
if (typeof transition[key] === 'function') {
listeners[key] = transition[key]
}
})
return h('transition', {
props: transitionProps,
on: listeners
}, [
h('router-view', data)
])
}
}

View File

@ -5,7 +5,7 @@
<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>
<p v-if="error.statusCode === 404"><nuxt-link class="error-link" to="/">Back to the home page</nuxt-link></p>
</div>
</div>
</template>

View File

@ -0,0 +1,9 @@
import Vue from 'vue'
export default {
name: 'nuxt-link',
functional: true,
render (h, { data, children }) {
return h('router-link', data, children)
}
}

View File

@ -1,15 +1,14 @@
<template>
<div>
<% if (loading) { %><nuxt-loading ref="loading"></nuxt-loading><% } %>
<transition :name="nuxt.transition.name" :mode="nuxt.transition.mode">
<router-view v-if="!nuxt.err"></router-view>
<nuxt-child v-if="!nuxt.err"></nuxt-child>
<nuxt-error v-if="nuxt.err" :error="nuxt.err"></nuxt-error>
</transition>
</div>
</template>
<script>
import Vue from 'vue'
import NuxtChild from './nuxt-child'
import NuxtError from '<%= components.ErrorPage %>'
<% if (loading) { %>import NuxtLoading from '<%= (typeof loading === "string" ? loading : "./nuxt-loading.vue") %>'<% } %>
@ -47,6 +46,7 @@ export default {
},
<% } %>
components: {
NuxtChild,
NuxtError<%= (loading ? ',\n NuxtLoading' : '') %>
}
}

View File

@ -5,11 +5,17 @@ import Meta from 'vue-meta'
import router from './router.js'
<% if (store) { %>import store from '~store/index.js'<% } %>
import NuxtContainer from './components/nuxt-container.vue'
import NuxtChild from './components/nuxt-child.js'
import NuxtLink from './components/nuxt-link.js'
import Nuxt from './components/nuxt.vue'
import App from '<%= appPath %>'
// Component: <nuxt-container>
Vue.component(NuxtContainer.name, NuxtContainer)
// Component: <nuxt-child>
Vue.component(NuxtChild.name, NuxtChild)
// Component: <nuxt-link>
Vue.component(NuxtLink.name, NuxtLink)
// Component: <nuxt>
Vue.component(Nuxt.name, Nuxt)
@ -43,16 +49,24 @@ const app = {
router,
<%= (store ? 'store,' : '') %>
_nuxt: {
transition: Object.assign({}, defaultTransition),
setTransition (transition) {
defaultTransition: defaultTransition,
transitions: [ defaultTransition ],
setTransitions (transitions) {
if (!Array.isArray(transitions)) {
transitions = [ transitions ]
}
transitions = transitions.map((transition) => {
if (!transition) {
transition = defaultTransition
} else if (typeof transition === 'string') {
transition = Object.assign({}, defaultTransition, { name: transition })
} else {
transition = Object.assign({}, defaultTransition, transition)
}
this.$options._nuxt.transition.name = transition.name
this.$options._nuxt.transition.mode = transition.mode
return transition
})
this.$options._nuxt.transitions = transitions
return transitions
},
err: null,
error (err) {

View File

@ -5,8 +5,24 @@ import Router from 'vue-router'
Vue.use(Router)
<% uniqBy(router.routes, '_name').forEach((route) => { %>
const <%= route._name %> = process.BROWSER_BUILD ? () => System.import('<%= route._component %>') : require('<%= route._component %>')
<%
function recursiveRoutes(routes, tab, components) {
var res = ''
routes.forEach((route, i) => {
components.push({ _name: route._name, component: route.component })
res += tab + '{\n'
res += tab + '\tpath: ' + JSON.stringify(route.path) + ',\n'
res += tab + '\tcomponent: ' + route._name
res += (route.name) ? ',\n\t' + tab + 'name: ' + JSON.stringify(route.name) : ''
res += (route.children) ? ',\n\t' + tab + 'children: [\n' + recursiveRoutes(routes[i].children, tab + '\t\t', components) + '\n\t' + tab + ']' : ''
res += '\n' + tab + '}' + (i + 1 === routes.length ? '' : ',\n')
})
return res
}
var _components = []
var _routes = recursiveRoutes(router.routes, '\t\t', _components)
uniqBy(_components, '_name').forEach((route) => { %>
const <%= route._name %> = process.BROWSER_BUILD ? () => System.import('<%= route.component %>') : require('<%= route.component %>')
<% }) %>
const scrollBehavior = (to, from, savedPosition) => {
@ -14,8 +30,11 @@ const scrollBehavior = (to, from, savedPosition) => {
// savedPosition is only available for popstate navigations.
return savedPosition
} else {
// Scroll to the top by default
let position = { x: 0, y: 0 }
let position = {}
// if no children detected
if (to.matched.length < 2) {
position = { x: 0, y: 0 }
}
// if link has anchor, scroll to anchor by returning the selector
if (to.hash) {
position = { selector: to.hash }
@ -30,13 +49,6 @@ export default new Router({
linkActiveClass: '<%= router.linkActiveClass %>',
scrollBehavior,
routes: [
<% router.routes.forEach((route, i) => { %>
{
path: '<%= route.path %>',
component: <%= route._name %><% if (route.name) { %>,
name: '<%= route.name %>'<% } %><% if (route.meta) { %>,
meta: <%= JSON.stringify(route.meta) %><% } %>
}<%= (i + 1 === router.routes.length ? '' : ',') %>
<% }) %>
<%= _routes %>
]
})

View File

@ -1,6 +1,7 @@
'use strict'
const debug = require('debug')('nuxt:render')
debug.color = 4 // force blue color
import Vue from 'vue'
import { stringify } from 'querystring'
import { omit } from 'lodash'
@ -60,8 +61,8 @@ export default context => {
<% } %>
return promise
.then(() => {
// Call data & fetch hooks on components matched by the route.
return Promise.all(Components.map((Component) => {
// Sanitize Components
Components = Components.map((Component) => {
let promises = []
if (!Component.options) {
Component = Vue.extend(Component)
@ -70,6 +71,24 @@ export default context => {
Component._Ctor = Component
Component.extendOptions = Component.options
}
return Component
})
// Call .validate()
let isValid = Components.some((Component) => {
if (typeof Component.options.validate !== 'function') return true
return Component.options.validate({
params: context.route.params || {},
query: context.route.query || {}
})
})
if (!isValid) {
// Call the 404 error by making the Components array empty
Components = []
return _app
}
// Call data & fetch hooks on components matched by the route.
return Promise.all(Components.map((Component) => {
let promises = []
const ctx = getContext(context)
if (Component.options.data && typeof Component.options.data === 'function') {
Component._data = Component.options.data

View File

@ -91,3 +91,235 @@ export function getLocation (base) {
export function urlJoin () {
return [].slice.call(arguments).join('/').replace(/\/+/g, '/')
}
// Imported from path-to-regexp
/**
* Compile a string to a template function for the path.
*
* @param {string} str
* @param {Object=} options
* @return {!function(Object=, Object=)}
*/
export function compile (str, options) {
return tokensToFunction(parse(str, options))
}
/**
* The main path matching regexp utility.
*
* @type {RegExp}
*/
const PATH_REGEXP = new RegExp([
// Match escaped characters that would otherwise appear in future matches.
// This allows the user to escape special characters that won't transform.
'(\\\\.)',
// Match Express-style parameters and un-named parameters with a prefix
// and optional suffixes. Matches appear as:
//
// "/:test(\\d+)?" => ["/", "test", "\d+", undefined, "?", undefined]
// "/route(\\d+)" => [undefined, undefined, undefined, "\d+", undefined, undefined]
// "/*" => ["/", undefined, undefined, undefined, undefined, "*"]
'([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))'
].join('|'), 'g')
/**
* Parse a string for the raw tokens.
*
* @param {string} str
* @param {Object=} options
* @return {!Array}
*/
function parse (str, options) {
var tokens = []
var key = 0
var index = 0
var path = ''
var defaultDelimiter = options && options.delimiter || '/'
var res
while ((res = PATH_REGEXP.exec(str)) != null) {
var m = res[0]
var escaped = res[1]
var offset = res.index
path += str.slice(index, offset)
index = offset + m.length
// Ignore already escaped sequences.
if (escaped) {
path += escaped[1]
continue
}
var next = str[index]
var prefix = res[2]
var name = res[3]
var capture = res[4]
var group = res[5]
var modifier = res[6]
var asterisk = res[7]
// Push the current path onto the tokens.
if (path) {
tokens.push(path)
path = ''
}
var partial = prefix != null && next != null && next !== prefix
var repeat = modifier === '+' || modifier === '*'
var optional = modifier === '?' || modifier === '*'
var delimiter = res[2] || defaultDelimiter
var pattern = capture || group
tokens.push({
name: name || key++,
prefix: prefix || '',
delimiter: delimiter,
optional: optional,
repeat: repeat,
partial: partial,
asterisk: !!asterisk,
pattern: pattern ? escapeGroup(pattern) : (asterisk ? '.*' : '[^' + escapeString(delimiter) + ']+?')
})
}
// Match any characters still remaining.
if (index < str.length) {
path += str.substr(index)
}
// If the path exists, push it onto the end.
if (path) {
tokens.push(path)
}
return tokens
}
/**
* Prettier encoding of URI path segments.
*
* @param {string}
* @return {string}
*/
function encodeURIComponentPretty (str) {
return encodeURI(str).replace(/[\/?#]/g, function (c) {
return '%' + c.charCodeAt(0).toString(16).toUpperCase()
})
}
/**
* Encode the asterisk parameter. Similar to `pretty`, but allows slashes.
*
* @param {string}
* @return {string}
*/
function encodeAsterisk (str) {
return encodeURI(str).replace(/[?#]/g, function (c) {
return '%' + c.charCodeAt(0).toString(16).toUpperCase()
})
}
/**
* Expose a method for transforming tokens into the path function.
*/
function tokensToFunction (tokens) {
// Compile all the tokens into regexps.
var matches = new Array(tokens.length)
// Compile all the patterns before compilation.
for (var i = 0; i < tokens.length; i++) {
if (typeof tokens[i] === 'object') {
matches[i] = new RegExp('^(?:' + tokens[i].pattern + ')$')
}
}
return function (obj, opts) {
var path = ''
var data = obj || {}
var options = opts || {}
var encode = options.pretty ? encodeURIComponentPretty : encodeURIComponent
for (var i = 0; i < tokens.length; i++) {
var token = tokens[i]
if (typeof token === 'string') {
path += token
continue
}
var value = data[token.name]
var segment
if (value == null) {
if (token.optional) {
// Prepend partial segment prefixes.
if (token.partial) {
path += token.prefix
}
continue
} else {
throw new TypeError('Expected "' + token.name + '" to be defined')
}
}
if (Array.isArray(value)) {
if (!token.repeat) {
throw new TypeError('Expected "' + token.name + '" to not repeat, but received `' + JSON.stringify(value) + '`')
}
if (value.length === 0) {
if (token.optional) {
continue
} else {
throw new TypeError('Expected "' + token.name + '" to not be empty')
}
}
for (var j = 0; j < value.length; j++) {
segment = encode(value[j])
if (!matches[i].test(segment)) {
throw new TypeError('Expected all "' + token.name + '" to match "' + token.pattern + '", but received `' + JSON.stringify(segment) + '`')
}
path += (j === 0 ? token.prefix : token.delimiter) + segment
}
continue
}
segment = token.asterisk ? encodeAsterisk(value) : encode(value)
if (!matches[i].test(segment)) {
throw new TypeError('Expected "' + token.name + '" to match "' + token.pattern + '", but received "' + segment + '"')
}
path += token.prefix + segment
}
return path
}
}
/**
* Escape a regular expression string.
*
* @param {string} str
* @return {string}
*/
function escapeString (str) {
return str.replace(/([.+*?=^!:${}()[\]|\/\\])/g, '\\$1')
}
/**
* Escape the capturing group by escaping special characters and meaning.
*
* @param {string} group
* @return {string}
*/
function escapeGroup (group) {
return group.replace(/([=!:$\/()])/g, '\\$1')
}

View File

@ -1,6 +1,7 @@
'use strict'
const debug = require('debug')('nuxt:build')
debug.color = 2 // force green color
const _ = require('lodash')
const co = require('co')
const chokidar = require('chokidar')
@ -9,10 +10,9 @@ const hash = require('hash-sum')
const pify = require('pify')
const webpack = require('webpack')
const { createBundleRenderer } = require('vue-server-renderer')
const { join, resolve, sep, posix } = require('path')
const { join, resolve, sep } = require('path')
const clientWebpackConfig = require('./webpack/client.config.js')
const serverWebpackConfig = require('./webpack/server.config.js')
const basename = posix.basename
const remove = pify(fs.remove)
const readFile = pify(fs.readFile)
const writeFile = pify(fs.writeFile)
@ -111,13 +111,6 @@ exports.build = function * () {
if (!this.dev) {
yield mkdirp(r(this.dir, '.nuxt/dist'))
}
// Resolve custom routes component path
this.options.router.routes.forEach((route) => {
if (route.component.slice(-4) !== '.vue') {
route.component = route.component + '.vue'
}
route.component = r(this.srcDir, route.component)
})
// Generate routes and interpret the template files
yield generateRoutesAndFiles.call(this)
/*
@ -144,15 +137,9 @@ function * generateRoutesAndFiles () {
** Generate routes based on files
*/
const files = yield glob('pages/**/*.vue', { cwd: this.srcDir })
let routes = []
files.forEach((file) => {
let path = file.replace(/^pages/, '').replace(/index\.vue$/, '/').replace(/\.vue$/, '').replace(/\/{2,}/g, '/')
let name = file.replace(/^pages/, '').replace(/\.vue$/, '').replace(/\/{2,}/g, '/').split('/').slice(1).join('-')
if (basename(path)[0] === '_') return
routes.push({ path: path, component: r(this.srcDir, file), name: name })
})
// Concat pages routes and custom routes in this.routes
this.routes = routes.concat(this.options.router.routes)
this.routes = _.uniq(_.map(files, (file) => {
return file.replace(/^pages/, '').replace(/\.vue$/, '').replace(/\/index/g, '').replace(/_/g, ':').replace('', '/').replace(/\/{2,}/g, '/')
}))
/*
** Interpret and move template files to .nuxt/
*/
@ -165,8 +152,10 @@ function * generateRoutesAndFiles () {
'server.js',
'utils.js',
'components/nuxt-container.vue',
'components/nuxt.vue',
'components/nuxt-loading.vue'
'components/nuxt-loading.vue',
'components/nuxt-child.js',
'components/nuxt-link.js',
'components/nuxt.vue'
]
let templateVars = {
uniqBy: _.uniqBy,
@ -192,23 +181,10 @@ function * generateRoutesAndFiles () {
templateVars.loading = templateVars.loading + '.vue'
}
// Format routes for the lib/app/router.js template
templateVars.router.routes = this.routes.map((route) => {
const r = Object.assign({}, route)
r._component = r.component
r._name = '_' + hash(r._component)
r.component = r._name
r.path = r.path.replace(/\\/g, '\\\\') // regex expression in route path escaping for lodash templating
return r
})
if (files.includes('pages/_app.vue')) {
templateVars.appPath = r(this.srcDir, 'pages/_app.vue')
}
templateVars.router.routes = createRoutes(files, this.srcDir)
if (fs.existsSync(join(this.srcDir, 'layouts', 'app.vue'))) {
templateVars.appPath = r(this.srcDir, 'layouts/app.vue')
}
if (files.includes('pages/_error.vue')) {
templateVars.components.ErrorPage = r(this.srcDir, 'pages/_error.vue')
}
if (fs.existsSync(join(this.srcDir, 'layouts', 'error.vue'))) {
templateVars.components.ErrorPage = r(this.srcDir, 'layouts/error.vue')
}
@ -223,6 +199,46 @@ function * generateRoutesAndFiles () {
yield moveTemplates
}
function createRoutes (files, srcDir) {
let routes = []
files.forEach((file) => {
let keys = file.replace(/^pages/, '').replace(/\.vue$/, '').replace(/\/{2,}/g, '/').split('/').slice(1)
let route = { name: '', path: '', component: r(srcDir, file), _name: null }
let parent = routes
keys.forEach((key, i) => {
route.name = route.name ? route.name + (key === 'index' ? '' : '-' + key.replace('_', '')) : key.replace('_', '')
let child = _.find(parent, { name: route.name })
if (child) {
if (!child.children) {
child.children = []
}
parent = child.children
} else {
route.path = route.path + (key === 'index' ? (i > 0 ? '' : '/') : '/' + key.replace('_', ':'))
}
})
route._name = '_' + hash(route.component)
// Order Routes path
if (_.last(keys)[0] === '_') {
parent.push(route)
} else {
parent.unshift(route)
}
})
return cleanChildrenRoutes(routes)
}
function cleanChildrenRoutes (routes, isChild = false) {
routes.forEach((route) => {
route.path = (isChild) ? route.path.replace('/', '') : route.path
if (route.children) {
delete route.name
route.children = cleanChildrenRoutes(route.children, true)
}
})
return routes
}
function getWebpackClientConfig () {
return clientWebpackConfig.call(this)
}

View File

@ -23,8 +23,11 @@ module.exports = function () {
output: {
publicPath: urlJoin(this.options.router.base, '/_nuxt/')
},
performance: {
hints: (this.dev ? false : 'warning')
},
resolve: {
extensions: ['.js', '.vue'],
extensions: ['.js', '.json', '.vue'],
// Disable for now
alias: {
'~': join(this.srcDir),

View File

@ -1,10 +1,18 @@
'use strict'
const { defaults } = require('lodash')
module.exports = function () {
let babelOptions = JSON.stringify(defaults(this.options.build.babel, {
presets: [
['es2015', { modules: false }],
'stage-2'
]
}))
let config = {
postcss: this.options.build.postcss,
loaders: {
'js': 'babel-loader?presets[]=es2015&presets[]=stage-2',
'js': 'babel-loader?' + babelOptions,
'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',

View File

@ -8,6 +8,7 @@ const pathToRegexp = require('path-to-regexp')
const _ = require('lodash')
const { resolve, join, dirname, sep } = require('path')
const { promisifyRouteParams } = require('./utils')
const { minify } = require('html-minifier')
const copy = pify(fs.copy)
const remove = pify(fs.remove)
const writeFile = pify(fs.writeFile)
@ -69,15 +70,15 @@ module.exports = function () {
*/
let routes = []
this.routes.forEach((route) => {
if (route.path.includes(':') || route.path.includes('*')) {
const routeParams = this.options.generate.routeParams[route.path]
if (route.includes(':') || route.includes('*')) {
const routeParams = this.options.generate.routeParams[route]
if (!routeParams) {
console.error(`Could not generate the dynamic route ${route.path}, please add the mapping params in nuxt.config.js (generate.routeParams).`) // eslint-disable-line no-console
console.error(`Could not generate the dynamic route ${route}, please add the mapping params in nuxt.config.js (generate.routeParams).`) // eslint-disable-line no-console
return process.exit(1)
}
const toPath = pathToRegexp.compile(route.path)
const toPath = pathToRegexp.compile(route)
routes = routes.concat(routeParams.map((params) => {
return Object.assign({}, route, { path: toPath(params) })
return toPath(params)
}))
} else {
routes.push(route)
@ -87,8 +88,28 @@ module.exports = function () {
while (routes.length) {
yield routes.splice(0, 500).map((route) => {
return co(function * () {
const { html } = yield self.renderRoute(route.path)
var path = join(route.path, sep, 'index.html') // /about -> /about/index.html
var { html } = yield self.renderRoute(route)
html = minify(html, {
collapseBooleanAttributes: true,
collapseWhitespace: true,
decodeEntities: true,
minifyCSS: true,
minifyJS: true,
processConditionalComments: true,
removeAttributeQuotes: true,
removeComments: true,
removeEmptyAttributes: true,
removeOptionalTags: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
removeTagWhitespace: true,
sortAttributes: true,
sortClassName: true,
trimCustomFragments: true,
useShortDoctype: true
})
var path = join(route, sep, 'index.html') // /about -> /about/index.html
debug('Generate file: ' + path)
path = join(distPath, path)
// Make sure the sub folders are created

View File

@ -1,5 +1,3 @@
'use strict'
const _ = require('lodash')
const co = require('co')
const fs = require('fs-extra')
@ -41,8 +39,7 @@ class Nuxt {
},
router: {
base: '/',
linkActiveClass: 'router-link-active',
routes: []
linkActiveClass: 'nuxt-link-active'
},
build: {}
}

View File

@ -1,4 +1,5 @@
const debug = require('debug')('nuxt:render')
debug.color = 4 // force blue color
const co = require('co')
const { urlJoin } = require('./utils')
const { getContext } = require('./utils')

View File

@ -1,6 +1,6 @@
{
"name": "nuxt",
"version": "0.8.8",
"version": "0.9.0",
"description": "A minimalistic framework for server-rendered Vue.js applications (inspired by Next.js)",
"contributors": [
{
@ -63,6 +63,7 @@
"fs-extra": "^1.0.0",
"glob": "^7.1.1",
"hash-sum": "^1.0.2",
"html-minifier": "^3.2.3",
"lodash": "^4.17.2",
"lru-cache": "^4.0.2",
"memory-fs": "^0.4.1",
@ -78,7 +79,7 @@
"vue-server-renderer": "^2.1.3",
"vue-template-compiler": "^2.1.3",
"vuex": "^2.0.0",
"webpack": "2.1.0-beta.27",
"webpack": "2.2.0-rc.0",
"webpack-dev-middleware": "^1.8.4",
"webpack-hot-middleware": "^2.13.2"
},