Skip to content
This repository has been archived by the owner on Jan 14, 2020. It is now read-only.

Commit

Permalink
File system routes (#96)
Browse files Browse the repository at this point in the history
This PR fixes #68. It may look huge, but the most part of the PR is the new example directory and tests.

- If there is a directory `pages`, its contents will be used to construct a router. This router will be injected by a plugin into entry object.
- Dynamic routes can be created with ~~`_param.vue` or~~ `[param].vue` (or `:param.vue`, although not on Windows).
- Child routes are created if there are both `some.vue` and `some/` in the same directory. If there's only `some/`, it also works, but adds prefixed routes to common list.
- User-provided entry is no more **required**. Ream will work with nothing but `pages/*.vue` (same as Nuxt). If entry is there, it is used. If there's no pages (and no router that came from entry), an exception will be thrown like it used to be (but better as it is more descriptive).
- It is possible to disable file system routes, change `pages` to something else, or change supported extensions. (I am also thinking about other options, e.g. `trailingSlashes: true`, or a custom filter function.)
- It is possible to disable *entrypoint*. (Not that it's really needed, but it came logically.)

I had to make some small changes in `create-app-template.js` to be able to allow plugins to override the router, and to make entry point optional, but most of the code went into a plugin.

The default naming `pages` (not `routes` and not `views`) was chosen because I believe it reflects better what users are putting there — pages that will show on their site. (Routes are URL matching rules, and views are from MVC world where there are also controllers in front — with `fsRoutes`, users don't create neither of these.)

`write-routes.js` is specifically made to be reusable. In my project, I need to have not one but two file system routers (for project.com and for app.project.com). It is now possible with a small ad-hoc plugin that calls `write-routes.js` twice.

`routes-template.js` is decoupled from Ream API for easier testing.
  • Loading branch information
IlyaSemenov authored and egoist committed Aug 15, 2018
1 parent 326e41f commit 48c3333
Show file tree
Hide file tree
Showing 43 changed files with 1,272 additions and 47 deletions.
23 changes: 8 additions & 15 deletions app/create-app-template.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ module.exports = api => {
import Vue from 'vue'
import Meta from 'vue-meta'
import Router from 'vue-router'
// eslint-disable-next-line import/no-unresolved
import _entry from '#app-entry'
import _entry from '#out/entry'
import { getRequireDefault } from '#app/utils'
Vue.config.productionTip = false
Expand Down Expand Up @@ -57,14 +56,9 @@ module.exports = api => {
context.entry = entry
}
if (__DEV__) {
if (!router) {
throw new Error('You must export "router" in entry file!')
}
}
const rootOptions = {
_isReamRoot: true
_isReamRoot: true,
router: entry.router || new Router({ mode: 'history' })
}
const getInitialDataContextFns = [
entry.getInitialDataContext
Expand All @@ -75,7 +69,6 @@ module.exports = api => {
const event = new Vue()
const enhanceContext = {
router,
rootOptions,
entry,
ssrContext: context,
Expand All @@ -90,10 +83,6 @@ module.exports = api => {
enhanceApp(enhanceContext, context)
if (extendRootOptions) {
extendRootOptions(rootOptions)
}
${[...enhanceAppFiles]
.map(file =>
`
Expand All @@ -104,11 +93,15 @@ module.exports = api => {
)
.join('\n')}
if (entry.extendRootOptions) {
entry.extendRootOptions(rootOptions)
}
const app = new Vue(rootOptions)
return {
app,
router,
router: rootOptions.router,
entry,
getInitialDataContextFns,
event,
Expand Down
14 changes: 9 additions & 5 deletions app/enhance-app.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,23 @@ const Error = {
) {
return (
<div>
<h1>{error.code}</h1>
<div>{error.message}</div>
<router-link to="/">Go Home!</router-link>
<h1>
{error.code}: {error.message}
</h1>
{__DEV__ &&
error.code === 404 &&
error.errorPath === '/' && (
<p>You must create pages/*.vue or export "router" in entry file!</p>
)}
</div>
)
}
}

export default ({ rootOptions, entry }, context) => {
const { router, root = Root, error = Error } = entry
const { root = Root, error = Error } = entry

const App = {
router,
dataStore: createDataStore(),
data() {
return {
Expand Down
24 changes: 24 additions & 0 deletions app/entry-template.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const fs = require('fs-extra')

// Poor man hot reload template for the app entry.
// Ideally, this fallback should be handled by webpack.
// However, at the moment there is no way to disable warning emitted by
// require.resolve('#app-entry') when the entry module does not exist.
//
// See discussion in https://github.com/ream/ream/pull/96

module.exports = api => {
if (
!(
api.options.entry &&
fs.pathExistsSync(api.resolveBaseDir(api.options.entry))
)
) {
return ``
}

return `
import entry from '#app-entry'
export default entry
`
}
24 changes: 24 additions & 0 deletions examples/fs-routes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Auto generates routes

## How to use

```bash
git clone https://github.com/ream/ream.git
```

Install dependencies:

```bash
cd examples/fs-routes
yarn
```

Run it:

```bash
# Start development server
yarn dev

# Start production server
yarn build && yarn start
```
12 changes: 12 additions & 0 deletions examples/fs-routes/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"private": true,
"name": "fs-routes",
"scripts": {
"dev": "ream dev",
"start": "ream start",
"build": "ream build"
},
"dependencies": {
"ream": "latest"
}
}
6 changes: 6 additions & 0 deletions examples/fs-routes/pages/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<template>
<div>
<h1>fs-routes example app</h1>
<router-link to="/user">Users</router-link>
</div>
</template>
6 changes: 6 additions & 0 deletions examples/fs-routes/pages/user.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<template>
<div>
<h1>Users</h1>
<router-view />
</div>
</template>
20 changes: 20 additions & 0 deletions examples/fs-routes/pages/user/[user_id].vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<template>
<div>
<h2 v-text="user" />
<ul>
<li><router-link :to="`/user/${user}`">Profile</router-link></li>
<li><router-link :to="`/user/${user}/friends`">Friends</router-link></li>
</ul>
<router-view />
</div>
</template>

<script>
export default {
computed: {
user () {
return this.$route.params.user_id
}
}
}
</script>
3 changes: 3 additions & 0 deletions examples/fs-routes/pages/user/[user_id]/friends.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<div>User {{ $route.params.user_id }} friends</div>
</template>
3 changes: 3 additions & 0 deletions examples/fs-routes/pages/user/[user_id]/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<div>User {{ $route.params.user_id }} profile</div>
</template>
17 changes: 17 additions & 0 deletions examples/fs-routes/pages/user/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<template>
<div>
<li v-for="user in users" :key="user">
<router-link :to="`/user/${user}`" v-text="user" />
</li>
</div>
</template>

<script>
export default {
data () {
return {
users: [ 'mary', 'ann', 'joe' ]
}
}
}
</script>
42 changes: 37 additions & 5 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const fs = require('fs-extra')
const Config = require('webpack-chain')
const express = require('express')
const chalk = require('chalk')
const chokidar = require('chokidar')
const merge = require('lodash.merge')
const { createBundleRenderer } = require('vue-server-renderer')
const loadConfig = require('./utils/loadConfig')
Expand Down Expand Up @@ -49,6 +50,11 @@ class Ream extends Event {
this.options = merge(
{
entry: 'index.js',
fsRoutes: {
baseDir: 'pages',
basePath: '/',
match: /\.(vue|js)$/i
},
plugins: [],
server: {
host: process.env.HOST || '0.0.0.0',
Expand Down Expand Up @@ -118,7 +124,8 @@ class Ream extends Event {
basePlugin,
require('./plugins/vuex'),
require('./plugins/apollo'),
require('./plugins/pwa')
require('./plugins/pwa'),
require('./plugins/fs-routes')
]
.concat(this.options.plugins)
.filter(Boolean)
Expand Down Expand Up @@ -217,14 +224,39 @@ class Ream extends Event {
}

async prepareFiles() {
const createAppFile = this.resolveOutDir('create-app.js')
await fs.ensureDir(path.dirname(createAppFile))
await fs.writeFile(
createAppFile,
await fs.ensureDir(this.resolveOutDir())
await Promise.all([
this.writeCreateAppFile(),
this.writeEntryFile(),
this.hooks.run('onPrepareFiles')
])
}

writeCreateAppFile() {
return fs.writeFile(
this.resolveOutDir('create-app.js'),
require('../app/create-app-template')(this)
)
}

async writeEntryFile() {
const writeFile = () =>
fs.writeFile(
this.resolveOutDir('entry.js'),
require('../app/entry-template')(this)
)
writeFile()
if (this.options.entry && this.options.dev) {
chokidar
.watch(this.resolveBaseDir(this.options.entry), {
disableGlobbing: true,
ignoreInitial: true
})
.on('add', writeFile)
.on('unlink', writeFile)
}
}

async getServer() {
const server = express()

Expand Down
1 change: 1 addition & 0 deletions lib/plugins/fs-routes/__test__/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Run `WRITE_EXPECTED_ROUTES=1 yarn test` to rewrite `_expected_routes.json`
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
{
"path": "/page",
"component": "#base/page.vue"
}
]
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[
{
"path": "/foo",
"component": "#base/foo.vue",
"children": [
{
"path": "bar",
"component": "#base/foo/bar.vue",
"children": [
{
"path": "baz",
"component": "#base/foo/bar/baz.vue",
"children": [
{
"path": "qux",
"component": "#base/foo/bar/baz/qux.vue"
}
]
}
]
}
]
}
]
Empty file.
Empty file.
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
{
"path": "/foo/bar/baz/qux",
"component": "#base/foo/bar/baz/qux.vue"
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[
{
"path": "/",
"component": "#base/index.vue"
},
{
"path": "/foo",
"component": "#base/foo.vue"
},
{
"path": "/user",
"component": "#base/user.vue",
"children": [
{
"path": ":user",
"component": "#base/user/[user].vue",
"children": [
{
"path": "",
"component": "#base/user/[user]/index.vue"
},
{
"path": "friends",
"component": "#base/user/[user]/friends.vue"
}
]
}
]
}
]
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Loading

0 comments on commit 48c3333

Please sign in to comment.