Skip to content

Commit

Permalink
first version
Browse files Browse the repository at this point in the history
  • Loading branch information
vonschau committed Oct 5, 2017
1 parent 7548fcb commit 9964379
Show file tree
Hide file tree
Showing 6 changed files with 5,471 additions and 103 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ npm-debug.log

# coverage
coverage

# JetBrains
.idea/
39 changes: 21 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,47 @@
# Dynamic Routes for Next.js
# Dynamic Routes with localization for Next.js

[![npm version](https://d25lcipzij17d.cloudfront.net/badge.svg?id=js&type=6&v=1.0.40&x2=0)](https://www.npmjs.com/package/next-routes) [![Coverage Status](https://coveralls.io/repos/github/fridays/next-routes/badge.svg)](https://coveralls.io/github/fridays/next-routes) [![Build Status](https://travis-ci.org/fridays/next-routes.svg?branch=master)](https://travis-ci.org/fridays/next-routes)
Based on [Next-Routes](https://github.com/fridays/next-routes) with these changes:

Easy to use universal dynamic routes for [Next.js](https://github.com/zeit/next.js)

- Express-style route and parameters matching
- Request handler middleware for express & co
- `Link` and `Router` that generate URLs by route definition
- No support for unnamed routes
- Route can be added only by name, locale and pattern (and optionally page) or options object
- `Link` and `Router` generate URLs only by route definition (name + params)
- URLs are prefixed with locale (ie. /en/about)

## How to use

Install:

```bash
npm install next-routes --save
npm install next-routes-with-locale --save
```

Create `routes.js` inside your project:

```javascript
const routes = module.exports = require('next-routes')()
const routes = module.exports = require('next-routes')({ locale: 'en' })

routes
.add('about')
.add('blog', '/blog/:slug')
.add('user', '/user/:id', 'profile')
.add('/:noname/:lang(en|es)/:wow+', 'complex')
.add({name: 'beta', pattern: '/v3', page: 'v3'})
.add('about', 'en', '/about')
.add('blog', 'en', '/blog/:slug')
.add('user', 'en', '/user/:id', 'profile')
.add({name: 'beta', locale: 'en', pattern: '/v3', page: 'v3'})
.add('about', 'cs', '/o-projektu')
.add('blog', 'cs', '/blog/:slug')
.add('user', 'cs', '/uzivatel/:id', 'profile')
.add({name: 'beta', locale: 'cs', pattern: '/v3', page: 'v3'})
```

This file is used both on the server and the client.

API:

- `routes.add(name, pattern = /name, page = name)`
- `routes.add(pattern, page)`
- `routes.add(name, locale, pattern = /name, page = name)`
- `routes.add(object)`

Arguments:

- `name` - Route name
- `locale` - Locale of the route
- `pattern` - Route pattern (like express, see [path-to-regexp](https://github.com/pillarjs/path-to-regexp))
- `page` - Page inside `./pages` to be rendered

Expand Down Expand Up @@ -113,7 +115,7 @@ export default () => (
<a>Hello world</a>
</Link>
or
<Link route='/blog/hello-world'>
<Link route='blog' locale='cs' params={{slug: 'ahoj-svete'}}>
<a>Hello world</a>
</Link>
</div>
Expand All @@ -123,8 +125,9 @@ export default () => (
API:

- `<Link route='name'>...</Link>`
- `<Link route='name' locale='locale'>...</Link>`
- `<Link route='name' params={params}> ... </Link>`
- `<Link route='/path/to/match'> ... </Link>`
- `<Link route='name' locale='locale' params={params}> ... </Link>`

Props:

Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "next-routes",
"version": "1.0.40",
"description": "Easy to use universal dynamic routes for Next.js",
"repository": "fridays/next-routes",
"name": "next-routes-with-locale",
"version": "1.0.0",
"description": "Easy to use locale-based dynamic routes for Next.js",
"repository": "vonschau/next-routes-with-locale",
"main": "dist",
"files": [
"dist"
Expand Down
58 changes: 32 additions & 26 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,53 @@
import pathToRegexp from 'path-to-regexp'
import React from 'react'
import {parse} from 'url'
import { parse } from 'url'
import NextLink from 'next/link'
import NextRouter from 'next/router'

module.exports = opts => new Routes(opts)

class Routes {
constructor ({
Link = NextLink,
Router = NextRouter
} = {}) {
Link = NextLink,
Router = NextRouter,
locale
} = {}) {
this.routes = []
this.Link = this.getLink(Link)
this.Router = this.getRouter(Router)
this.locale = locale
}

add (name, pattern, page) {
add (name, locale = this.locale, pattern, page) {
let options
if (name instanceof Object) {
options = name

if (!options.name) {
throw new Error(`Unnamed routes not supported`)
}

name = options.name
locale = options.locale || this.locale
} else {
if (name[0] === '/') {
page = pattern
pattern = name
name = null
}
options = {name, pattern, page}
options = {name, locale, pattern, page}
}

if (this.findByName(name)) {
if (this.findByName(name, locale)) {
throw new Error(`Route "${name}" already exists`)
}

this.routes.push(new Route(options))
return this
}

findByName (name) {
setLocale (locale) {
this.locale = locale
}

findByName (name, locale) {
if (name) {
return this.routes.filter(route => route.name === name)[0]
return this.routes.filter(route => route.name === name && route.locale === locale)[0]
}
}

Expand All @@ -56,16 +63,13 @@ class Routes {
}, {query, parsedUrl})
}

findAndGetUrls (nameOrUrl, params) {
const route = this.findByName(nameOrUrl)
findAndGetUrls (name, locale, params) {
const route = this.findByName(name, locale)

if (route) {
return {route, urls: route.getUrls(params), byName: true}
} else {
const {route, query} = this.match(nameOrUrl)
const href = route ? route.getHref(query) : nameOrUrl
const urls = {href, as: nameOrUrl}
return {route, urls}
throw new Error(`Route "${name}" not found`)
}
}

Expand All @@ -89,11 +93,12 @@ class Routes {

getLink (Link) {
const LinkRoutes = props => {
const {route, params, to, ...newProps} = props
const {route, l, params, to, ...newProps} = props
const nameOrUrl = route || to
const locale = l || this.locale

if (nameOrUrl) {
Object.assign(newProps, this.findAndGetUrls(nameOrUrl, params).urls)
Object.assign(newProps, this.findAndGetUrls(nameOrUrl, locale, params).urls)
}

return <Link {...newProps} />
Expand All @@ -102,8 +107,8 @@ class Routes {
}

getRouter (Router) {
const wrap = method => (route, params, options) => {
const {byName, urls: {as, href}} = this.findAndGetUrls(route, params)
const wrap = method => (route, locale, params, options) => {
const {byName, urls: {as, href}} = this.findAndGetUrls(route, locale, params)
return Router[method](href, as, byName ? options : params)
}

Expand All @@ -115,12 +120,13 @@ class Routes {
}

class Route {
constructor ({name, pattern, page = name}) {
constructor ({name, locale, pattern, page = name}) {
if (!name && !page) {
throw new Error(`Missing page to render for route "${pattern}"`)
}

this.name = name
this.locale = locale
this.pattern = pattern || `/${name}`
this.page = page.replace(/(^|\/)index$/, '').replace(/^\/?/, '/')
this.regex = pathToRegexp(this.pattern, this.keys = [])
Expand All @@ -146,7 +152,7 @@ class Route {
}

getAs (params = {}) {
const as = this.toPath(params)
const as = '/' + this.locale + this.toPath(params)
const keys = Object.keys(params)
const qsKeys = keys.filter(key => this.keyNames.indexOf(key) === -1)

Expand Down
82 changes: 27 additions & 55 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import nextRoutes from '../dist'
const renderer = new ReactShallowRenderer()

const setupRoute = (...args) => {
const routes = nextRoutes().add(...args)
const routes = nextRoutes({locale: 'en'}).add(...args)
const route = routes.routes[routes.routes.length - 1]
return {routes, route}
}
Expand All @@ -20,56 +20,40 @@ describe('Routes', () => {
}

test('add with object', () => {
setup({name: 'a'}).testRoute({name: 'a', pattern: '/a', page: '/a'})
})

test('add with name', () => {
setup('a').testRoute({name: 'a', pattern: '/a', page: '/a'})
setup({name: 'a', locale: 'en'}).testRoute({name: 'a', locale: 'en', pattern: '/a', page: '/a'})
})

test('add with name and pattern', () => {
setup('a', '/:a').testRoute({name: 'a', pattern: '/:a', page: '/a'})
setup('a', 'en', '/:a').testRoute({name: 'a', locale: 'en', pattern: '/:a', page: '/a'})
})

test('add with name, pattern and page', () => {
setup('a', '/:a', 'b').testRoute({name: 'a', pattern: '/:a', page: '/b'})
})

test('add with pattern and page', () => {
setup('/:a', 'b').testRoute({name: null, pattern: '/:a', page: '/b'})
})

test('add with only pattern throws', () => {
expect(() => setup('/:a')).toThrow()
setup('a', 'en', '/:a', 'b').testRoute({name: 'a', locale: 'en', pattern: '/:a', page: '/b'})
})

test('add with existing name throws', () => {
expect(() => nextRoutes().add('a').add('a')).toThrow()
})

test('add multiple unnamed routes', () => {
expect(nextRoutes().add('/a', 'a').add('/b', 'b').routes.length).toBe(2)
expect(() => nextRoutes().add('a', 'en').add('a', 'en')).toThrow()
})

test('page with leading slash', () => {
setup('a', '/', '/b').testRoute({page: '/b'})
setup('a', 'en', '/', '/b').testRoute({page: '/b'})
})

test('page index becomes /', () => {
setup('index', '/').testRoute({page: '/'})
setup('index', 'en', '/').testRoute({page: '/'})
})

test('match and merge params into query', () => {
const routes = nextRoutes().add('a').add('b', '/b/:b').add('c')
const routes = nextRoutes().add('a', 'en').add('b', 'en', '/b/:b').add('c', 'en')
expect(routes.match('/b/b?b=x&c=c').query).toMatchObject({b: 'b', c: 'c'})
})

test('generate urls from params', () => {
const {route} = setup('a', '/a/:b/:c+')
const {route} = setup('a', 'en', '/a/:b/:c+')
const params = {b: 'b', c: [1, 2], d: 'd'}
const expected = {as: '/a/b/1/2?d=d', href: '/a?b=b&c=1%2F2&d=d'}
const expected = {as: '/en/a/b/1/2?d=d', href: '/a?b=b&c=1%2F2&d=d'}
expect(route.getUrls(params)).toEqual(expected)
expect(setup('a').route.getUrls()).toEqual({as: '/a', href: '/a?'})
expect(setup('a', 'en').route.getUrls()).toEqual({as: '/en/a', href: '/a?'})
})

test('with custom Link and Router', () => {
Expand Down Expand Up @@ -123,30 +107,19 @@ describe('Link', () => {
expect(actual.type).toBe(NextLink)
expect(actual.props).toEqual({...props, ...expected})
}
return {routes, route, testLink}
const testLinkException = (addProps) => {
expect(() => renderer.render(<Link {...props} {...addProps} />)).toThrow()
}
return {routes, route, testLink, testLinkException}
}

test('with name and params', () => {
const {route, testLink} = setup('a', '/a/:b')
const {route, testLink} = setup('a', 'en', '/a/:b')
testLink({route: 'a', params: {b: 'b'}}, route.getUrls({b: 'b'}))
})

test('with route url', () => {
const {routes, route, testLink} = setup('/a/:b', 'a')
testLink({route: '/a/b'}, route.getUrls(routes.match('/a/b').query))
})

test('with to', () => {
const {routes, route, testLink} = setup('/a/:b', 'a')
testLink({to: '/a/b'}, route.getUrls(routes.match('/a/b').query))
})

test('with route not found', () => {
setup('a').testLink({route: '/b'}, {href: '/b', as: '/b'})
})

test('without route', () => {
setup('a').testLink({href: '/'}, {href: '/'})
setup('a', 'en').testLinkException({route: 'b'})
})
})

Expand All @@ -162,23 +135,22 @@ describe(`Router ${routerMethods.join(', ')}`, () => {
expect(Router[method]).toBeCalledWith(...expected)
})
}
return {routes, route, testMethods}
const testException = (args) => {
routerMethods.forEach(method => {
const Router = routes.getRouter({[method]: jest.fn()})
expect(() => Router[`${method}Route`](...args)).toThrow()
})
}
return {routes, route, testMethods, testException}
}

test('with name and params', () => {
const {route, testMethods} = setup('a', '/a/:b')
const {route, testMethods} = setup('a', 'en', '/a/:b')
const {as, href} = route.getUrls({b: 'b'})
testMethods(['a', {b: 'b'}, {}], [href, as, {}])
})

test('with route url', () => {
const {routes, testMethods} = setup('/a', 'a')
const {route, query} = routes.match('/a')
const {as, href} = route.getUrls(query)
testMethods(['/a', {}], [href, as, {}])
testMethods(['a', 'en', {b: 'b'}, {}], [href, as, {}])
})

test('with route not found', () => {
setup('a').testMethods(['/b', {}], ['/b', '/b', {}])
setup('a', 'en').testException(['/b', 'en', {}])
})
})
Loading

0 comments on commit 9964379

Please sign in to comment.