Skip to content

Commit

Permalink
Add synchronous mode as an add-on (#164)
Browse files Browse the repository at this point in the history
* Add a `sync` plugin with a very similar API except synchronous

* Run `npm run build`

* Update docs

* Rename sync to UniversalRouterSync

* Reduce bundle size of sync router

* Export UniversalRouterSync in umd build
  • Loading branch information
futpib authored and frenzzy committed Feb 20, 2019
1 parent d5e32ce commit 2f445f3
Show file tree
Hide file tree
Showing 17 changed files with 1,470 additions and 63 deletions.
485 changes: 485 additions & 0 deletions dist/universal-router-sync.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions dist/universal-router-sync.js.map

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions dist/universal-router-sync.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions dist/universal-router-sync.min.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/universal-router.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/universal-router.min.js.map

Large diffs are not rendered by default.

59 changes: 44 additions & 15 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ Second `options` argument is optional where you can pass the following:
import UniversalRouter from 'universal-router'

const routes = {
path: '/page', // string or regexp or array of them, optional
path: '/page', // string or regexp or array of them, optional
name: 'page', // unique string, optional
parent: null, // route object or null, automatically filled by the router
children: [], // array of route objects or null, optional
children: [], // array of route objects or null, optional
action(context, params) { // function, optional

// action method should return anything except `null` or `undefined` to be resolved by router
Expand Down Expand Up @@ -59,19 +59,14 @@ const router = new UniversalRouter(routes, options)

## `router.resolve({ pathname, ...context })``Promise<any>`

Traverses the list of routes in the order they are defined until it finds the first route that
matches provided URL path string and whose `action` function returns anything other than `null` or `undefined`.
Traverses the list of routes in the order they are defined until it finds the first route
that matches provided URL path string and whose `action` function returns anything
other than `null` or `undefined`.

```js
const router = new UniversalRouter([
{
path: '/one',
action: () => 'Page One',
},
{
path: '/two',
action: () => `Page Two`,
},
{ path: '/one', action: () => 'Page One' },
{ path: '/two', action: () => 'Page Two' },
])

router.resolve({ pathname: '/one' })
Expand Down Expand Up @@ -130,7 +125,8 @@ router.resolve({ pathname: '/hello/john' })
// => Welcome, john!
```

Alternatively, captured parameters can be accessed via the second argument to an action method like so:
Alternatively, captured parameters can be accessed via the second argument
to an action method like so:

```js
const router = new UniversalRouter({
Expand All @@ -146,8 +142,9 @@ router.resolve({ pathname: '/hello/john' })
Router preserves the `context.params` values from the parent router.
If the parent and the child have conflicting param names, the child's value take precedence.

This functionality is powered by [path-to-regexp](https://github.com/pillarjs/path-to-regexp) npm module
and works the same way as the routing solutions in many popular JavaScript frameworks such as Express and Koa.
This functionality is powered by [path-to-regexp](https://github.com/pillarjs/path-to-regexp)
npm module and works the same way as the routing solutions in many popular JavaScript frameworks
such as [Express](https://expressjs.com/) and [Koa](https://koajs.com/).
Also check out online [router tester](http://forbeslindesay.github.io/express-route-tester/).

## Context
Expand Down Expand Up @@ -283,6 +280,38 @@ const middlewareRoute = {
}
```

## Synchronous mode

For most application a Promise-based asynchronous API is the best choice.
But if you absolutely have to resolve your routes synchronously,
this option is available as an add-on.

Simply import `universal-router/sync` instead of `universal-router`
and you'll get almost the same API, but without the `Promise` support.

```diff
-import UniversalRouter from 'universal-router'
+import UniversalRouterSync from 'universal-router/sync'
```

Now the `resolve` method will synchronously return whatever
the matching route action returned (or throw an error).

```js
const router = new UniversalRouterSync([
{ path: '/one', action: () => 'Page One' },
{ path: '/two', action: () => 'Page Two' },
])

const result = router.resolve({ pathname: '/one' })

console.log(result) // => Page One
```

This implies that your `action` functions have to be synchronous too.

The `context.next` function will be synchronous too and will return whatever the matching action returned.

## URL Generation

In most web applications it's much simpler to just use a string for hyperlinks.
Expand Down
4 changes: 3 additions & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ This module contains a `UniversalRouter` class with a single `router.resolve` me
the list of routes, until it finds the first route matching the provided URL path string and whose action method
returns anything other than `null` or `undefined`. Each route is just a plain JavaScript object having `path`,
`action`, and `children` (optional) properties.

```js
import UniversalRouter from 'universal-router'

Expand All @@ -39,6 +39,8 @@ also provide single-file distributions, which are hosted on a [CDN](https://unpk
```html
<script src="https://unpkg.com/universal-router/universal-router.js"></script>
<script src="https://unpkg.com/universal-router/universal-router.min.js"></script>
<script src="https://unpkg.com/universal-router/universal-router-sync.js"></script>
<script src="https://unpkg.com/universal-router/universal-router-sync.min.js"></script>
<script src="https://unpkg.com/universal-router/universal-router-generate-urls.js"></script>
<script src="https://unpkg.com/universal-router/universal-router-generate-urls.min.js"></script>
```
Expand Down
25 changes: 12 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,25 +27,24 @@
"path-to-regexp": "^3.0.0"
},
"devDependencies": {
"@babel/core": "^7.2.2",
"@babel/preset-env": "^7.2.3",
"@babel/core": "^7.3.3",
"@babel/preset-env": "^7.3.1",
"@babel/register": "^7.0.0",
"babel-core": "^7.0.0-0",
"babel-jest": "^23.6.0",
"eslint": "^5.12.0",
"babel-jest": "^24.1.0",
"eslint": "^5.14.1",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-config-prettier": "^3.4.0",
"eslint-plugin-import": "^2.14.0",
"eslint-config-prettier": "^4.0.0",
"eslint-plugin-import": "^2.16.0",
"fs-extra": "^7.0.1",
"husky": "^1.3.1",
"jest": "^23.6.0",
"prettier": "^1.15.3",
"rollup": "^1.1.0",
"rollup-plugin-babel": "^4.3.0",
"jest": "^24.1.0",
"prettier": "^1.16.4",
"rollup": "^1.2.2",
"rollup-plugin-babel": "^4.3.2",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-node-resolve": "^4.0.0",
"rollup-plugin-uglify": "^6.0.1",
"typescript": "^3.2.2"
"rollup-plugin-uglify": "^6.0.2",
"typescript": "^3.3.3"
},
"scripts": {
"lint": "node tools/lint",
Expand Down
20 changes: 2 additions & 18 deletions src/UniversalRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,8 @@

import pathToRegexp from 'path-to-regexp'
import matchRoute from './matchRoute'

function resolveRoute(context, params) {
if (typeof context.route.action === 'function') {
return context.route.action(context, params)
}
return undefined
}

function isChildRoute(parentRoute, childRoute) {
let route = childRoute
while (route) {
route = route.parent
if (route === parentRoute) {
return true
}
}
return false
}
import resolveRoute from './resolveRoute'
import isChildRoute from './isChildRoute'

class UniversalRouter {
constructor(routes, options = {}) {
Expand Down
55 changes: 55 additions & 0 deletions src/UniversalRouterSync.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* Universal Router (https://www.kriasoft.com/universal-router/)
*
* Copyright (c) 2015-present Kriasoft.
*
* This source code is licensed under the MIT license found in the
* LICENSE.txt file in the root directory of this source tree.
*/

import pathToRegexp = require('path-to-regexp')

export interface Params {
[paramName: string]: any
}

export interface Context {
[propName: string]: any
}

export interface ResolveContext extends Context {
pathname: string
}

export interface RouteContext<C extends Context, R = any> extends ResolveContext {
router: UniversalRouter<C, R>
route: Route
baseUrl: string
path: string
params: Params
keys: pathToRegexp.Key[]
next: (resume?: boolean) => R
}

export interface Route<C extends Context = any, R = any> {
path?: string | RegExp | Array<string | RegExp>
name?: string
parent?: Route | null
children?: Routes<C, R> | null
action?: (context: RouteContext<C, R> & C, params: Params) => R | void
}

export type Routes<C extends Context = Context, R = any> = Array<Route<C, R>>

export interface Options<C extends Context = Context, R = any> {
context?: C
baseUrl?: string
resolveRoute?: (context: C & RouteContext<C, R>, params: Params) => any
errorHandler?: (error: Error & { status?: number }, context: C & RouteContext<C, R>) => any
}

export default class UniversalRouter<C extends Context = Context, R = any> {
static pathToRegexp: typeof pathToRegexp
constructor(routes: Route<C, R> | Routes<C, R>, options?: Options<C>)
resolve(pathnameOrContext: string | ResolveContext): R
}
90 changes: 90 additions & 0 deletions src/UniversalRouterSync.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* Universal Router (https://www.kriasoft.com/universal-router/)
*
* Copyright (c) 2015-present Kriasoft.
*
* This source code is licensed under the MIT license found in the
* LICENSE.txt file in the root directory of this source tree.
*/

import pathToRegexp from 'path-to-regexp'
import matchRoute from './matchRoute'
import resolveRoute from './resolveRoute'
import isChildRoute from './isChildRoute'

class UniversalRouterSync {
constructor(routes, options = {}) {
if (!routes || typeof routes !== 'object') {
throw new TypeError('Invalid routes')
}

this.baseUrl = options.baseUrl || ''
this.errorHandler = options.errorHandler
this.resolveRoute = options.resolveRoute || resolveRoute
this.context = { router: this, ...options.context }
this.root = Array.isArray(routes) ? { path: '', children: routes, parent: null } : routes
this.root.parent = null
}

resolve(pathnameOrContext) {
const context = {
...this.context,
...(typeof pathnameOrContext === 'string'
? { pathname: pathnameOrContext }
: pathnameOrContext),
}
const match = matchRoute(
this.root,
this.baseUrl,
context.pathname.substr(this.baseUrl.length),
[],
null,
)
const resolve = this.resolveRoute
let matches = null
let nextMatches = null
let currentContext = context

function next(resume, parent = matches.value.route, prevResult) {
const routeToSkip = prevResult === null && matches.value.route
matches = nextMatches || match.next(routeToSkip)
nextMatches = null

if (!resume) {
if (matches.done || !isChildRoute(parent, matches.value.route)) {
nextMatches = matches
return null
}
}

if (matches.done) {
const error = new Error('Route not found')
error.status = 404
throw error
}

currentContext = { ...context, ...matches.value }

const result = resolve(currentContext, matches.value.params)
if (result !== null && result !== undefined) {
return result
}
return next(resume, parent, result)
}

context.next = next

try {
return next(true, this.root)
} catch (error) {
if (this.errorHandler) {
return this.errorHandler(error, currentContext)
}
throw error
}
}
}

UniversalRouterSync.pathToRegexp = pathToRegexp

export default UniversalRouterSync
21 changes: 21 additions & 0 deletions src/isChildRoute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Universal Router (https://www.kriasoft.com/universal-router/)
*
* Copyright (c) 2015-present Kriasoft.
*
* This source code is licensed under the MIT license found in the
* LICENSE.txt file in the root directory of this source tree.
*/

function isChildRoute(parentRoute, childRoute) {
let route = childRoute
while (route) {
route = route.parent
if (route === parentRoute) {
return true
}
}
return false
}

export default isChildRoute
17 changes: 17 additions & 0 deletions src/resolveRoute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Universal Router (https://www.kriasoft.com/universal-router/)
*
* Copyright (c) 2015-present Kriasoft.
*
* This source code is licensed under the MIT license found in the
* LICENSE.txt file in the root directory of this source tree.
*/

function resolveRoute(context, params) {
if (typeof context.route.action === 'function') {
return context.route.action(context, params)
}
return undefined
}

export default resolveRoute
Loading

0 comments on commit 2f445f3

Please sign in to comment.