├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | tmp/ 4 | dist/ 5 | npm-debug.log* 6 | .DS_Store 7 | .nyc_output 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | node_js: 2 | - "4" 3 | - "5" 4 | - "6" 5 | - "7" 6 | sudo: false 7 | language: node_js 8 | script: "npm run test" 9 | # after_success: "npm i -g codecov && npm run coverage && codecov" 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Yoshua Wuyts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nanorouter [![stability][0]][1] 2 | [![npm version][2]][3] [![build status][4]][5] 3 | [![downloads][8]][9] [![js-standard-style][10]][11] 4 | 5 | Smol frontend router 6 | 7 | ## Usage 8 | ```js 9 | var nanorouter = require('nanorouter') 10 | var router = nanorouter({ default: '/404' }) 11 | 12 | router.on('/foo', function (params) { 13 | console.log('hit /foo') 14 | }) 15 | router.on('/foo/:bar', function (params) { 16 | console.log('hit a route with params', params.bar) 17 | }) 18 | router.on('/foo#baz', function (params) { 19 | console.log('we do hash routes too!') 20 | }) 21 | router.on('/foo/*', function (params) { 22 | console.log('and even wildcards', params.wildcard) 23 | }) 24 | 25 | router.emit('/foo/hello-planet') 26 | ``` 27 | 28 | ## FAQ 29 | ### How is this different from sheet-router? 30 | `sheet-router` does slightly more and has a different syntax. This router is 31 | lighter, faster and covers less concerns. They're pretty similar under the hood 32 | though. 33 | 34 | ## API 35 | ### `router = nanorouter([opts])` 36 | Create a new router. `opts` can be: 37 | - __opts.default:__ set a default handler in case no route matches. Defaults to 38 | `/404` 39 | 40 | ### `router.on(routename, handler(params))` 41 | Register a handler on a routename. The handler receives an object with params 42 | on each render. A result can be `return`ed the caller function. 43 | 44 | ### `result = router.emit(routename)` 45 | Call a handler for a `routename`. If no handler matches, the handler specified 46 | in `opts.default` will be called. If no default handler matches, an error will 47 | be thrown. Results returned from the called handler will be returned from this 48 | function. 49 | 50 | ### `matchedRoute = router.match(route)` 51 | Matches a route and returns an object. The returned object contains the properties `{cb, params, route}`. This method does not invoke the callback of a route. If no route matches, the route specified in `opts.default` will be returned. If no default route matches, an error will be thrown. 52 | 53 | Note that `router()` does not affect browser history. If you would like to 54 | add or modify history entries when you change routes, you should use 55 | [`history.pushState()` and `history.replaceState()`](https://developer.mozilla.org/en-US/docs/Web/API/History_API#Adding_and_modifying_history_entries) 56 | alongside `router()`. 57 | 58 | ## See Also 59 | - [yoshuawuyts/sheet-router](https://github.com/yoshuawuyts/sheet-router) 60 | - [yoshuawuyts/wayfarer](https://github.com/yoshuawuyts/wayfarer) 61 | 62 | ## License 63 | [MIT](https://tldrlegal.com/license/mit-license) 64 | 65 | [0]: https://img.shields.io/badge/stability-experimental-orange.svg?style=flat-square 66 | [1]: https://nodejs.org/api/documentation.html#documentation_stability_index 67 | [2]: https://img.shields.io/npm/v/nanorouter.svg?style=flat-square 68 | [3]: https://npmjs.org/package/nanorouter 69 | [4]: https://img.shields.io/travis/choojs/nanorouter/master.svg?style=flat-square 70 | [5]: https://travis-ci.org/choojs/nanorouter 71 | [6]: https://img.shields.io/codecov/c/github/choojs/nanorouter/master.svg?style=flat-square 72 | [7]: https://codecov.io/github/choojs/nanorouter 73 | [8]: http://img.shields.io/npm/dm/nanorouter.svg?style=flat-square 74 | [9]: https://npmjs.org/package/nanorouter 75 | [10]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square 76 | [11]: https://github.com/feross/standard 77 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert') 2 | var wayfarer = require('wayfarer') 3 | 4 | // electron support 5 | var isLocalFile = (/file:\/\//.test( 6 | typeof window === 'object' && 7 | window.location && 8 | window.location.origin 9 | )) 10 | 11 | /* eslint-disable no-useless-escape */ 12 | var electron = '^(file:\/\/|\/)(.*\.html?\/?)?' 13 | var protocol = '^(http(s)?(:\/\/))?(www\.)?' 14 | var domain = '[a-zA-Z0-9-_\.]+(:[0-9]{1,5})?(\/{1})?' 15 | var qs = '[\?].*$' 16 | /* eslint-enable no-useless-escape */ 17 | 18 | var stripElectron = new RegExp(electron) 19 | var prefix = new RegExp(protocol + domain) 20 | var normalize = new RegExp('#') 21 | var suffix = new RegExp(qs) 22 | 23 | module.exports = Nanorouter 24 | 25 | function Nanorouter (opts) { 26 | if (!(this instanceof Nanorouter)) return new Nanorouter(opts) 27 | opts = opts || {} 28 | this.router = wayfarer(opts.default || '/404') 29 | } 30 | 31 | Nanorouter.prototype.on = function (routename, listener) { 32 | assert.equal(typeof routename, 'string') 33 | routename = routename.replace(/^[#/]/, '') 34 | this.router.on(routename, listener) 35 | } 36 | 37 | Nanorouter.prototype.emit = function (routename) { 38 | assert.equal(typeof routename, 'string') 39 | routename = pathname(routename, isLocalFile) 40 | return this.router.emit(routename) 41 | } 42 | 43 | Nanorouter.prototype.match = function (routename) { 44 | assert.equal(typeof routename, 'string') 45 | routename = pathname(routename, isLocalFile) 46 | return this.router.match(routename) 47 | } 48 | 49 | // replace everything in a route but the pathname and hash 50 | function pathname (routename, isElectron) { 51 | if (isElectron) routename = routename.replace(stripElectron, '') 52 | else routename = routename.replace(prefix, '') 53 | return decodeURI(routename.replace(suffix, '').replace(normalize, '/')) 54 | } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nanorouter", 3 | "description": "Smol frontend router", 4 | "repository": "choojs/nanorouter", 5 | "version": "4.0.0", 6 | "browser": { 7 | "assert": "nanoassert" 8 | }, 9 | "scripts": { 10 | "deps": "dependency-check . && dependency-check . --extra --no-dev -i nanoassert", 11 | "start": "node .", 12 | "test": "standard && npm run deps && tape test.js" 13 | }, 14 | "dependencies": { 15 | "nanoassert": "^1.1.0", 16 | "wayfarer": "^7.0.0" 17 | }, 18 | "devDependencies": { 19 | "dependency-check": "^2.8.0", 20 | "nyc": "^10.1.2", 21 | "standard": "^9.0.1", 22 | "tape": "^4.8.0" 23 | }, 24 | "keywords": [ 25 | "router", 26 | "frontend", 27 | "browser" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var nanorouter = require('./') 3 | function noop () {} 4 | 5 | tape('router', function (t) { 6 | t.test('.on() throws type errors for invalid parameters', function (t) { 7 | t.plan(2) 8 | var r = nanorouter() 9 | t.throws(r.on.bind(r, 123), /string/, 'route must be a string') 10 | t.throws(r.on.bind(r, '/', 123), /function/, 'handler must be a function') 11 | }) 12 | 13 | t.test('.emit() throws if if no route is found', function (t) { 14 | t.plan(2) 15 | var r = nanorouter() 16 | t.throws(r.emit.bind(r, '/'), /route '\/' did not match/) 17 | t.throws(r.emit.bind(r, '/test'), /route '\/test' did not match/) 18 | }) 19 | 20 | t.test('.emit() should match a path', function (t) { 21 | t.plan(2) 22 | var r = nanorouter() 23 | r.on('/foo', function () { 24 | t.pass('called') 25 | }) 26 | r.on('/foo/bar', function () { 27 | t.pass('called') 28 | }) 29 | r.emit('/foo') 30 | r.emit('/foo/bar') 31 | }) 32 | 33 | t.test('.emit() should fallback to a default path', function (t) { 34 | t.plan(2) 35 | var r1 = nanorouter() 36 | var r2 = nanorouter({default: '/custom-error'}) 37 | r1.on('/404', function () { 38 | t.pass('default called') 39 | }) 40 | r2.on('/custom-error', function () { 41 | t.pass('custom error called') 42 | }) 43 | r1.emit('/nope') 44 | r2.emit('/custom-error') 45 | }) 46 | 47 | t.test('.emit() should match partials', function (t) { 48 | t.plan(2) 49 | var r = nanorouter() 50 | r.on('/:foo/:bar', function (param) { 51 | t.equal(param.foo, 'baz', 'first param matched') 52 | t.equal(param.bar, 'qux', 'second param matched') 53 | }) 54 | r.emit('/baz/qux') 55 | }) 56 | 57 | t.test('.emit() should match a hash', function (t) { 58 | t.plan(1) 59 | var r = nanorouter() 60 | r.on('#test', function () { 61 | t.pass('called') 62 | }) 63 | r.emit('#test') 64 | }) 65 | 66 | t.test('.match() should match a path with utf-8 characters', function (t) { 67 | t.plan(1) 68 | var r = nanorouter() 69 | r.on('/foobær', function () { 70 | t.fail('accidentally called') 71 | }) 72 | t.ok(r.match(encodeURI('/foobær'))) 73 | }) 74 | 75 | t.test('.match() should match a path', function (t) { 76 | t.plan(2) 77 | var r = nanorouter() 78 | r.on('/foo', function () { 79 | t.fail('accidentally called') 80 | }) 81 | r.on('/foo/bar', function () { 82 | t.fail('accidentally called') 83 | }) 84 | t.ok(r.match('/foo')) 85 | t.ok(r.match('/foo/bar')) 86 | }) 87 | 88 | t.test('.match() returns a an object with a handler', function (t) { 89 | t.plan(1) 90 | var r = nanorouter() 91 | r.on('/:foo/:bar', function () { 92 | t.pass('called') 93 | }) 94 | r.match('/baz/qux').cb() 95 | }) 96 | 97 | t.test('.match() should match partials', function (t) { 98 | t.plan(3) 99 | var r = nanorouter() 100 | r.on('/:foo/:bar', noop) 101 | var matched = r.match('/baz/qux') 102 | t.equal(matched.params.foo, 'baz') 103 | t.equal(matched.params.bar, 'qux') 104 | t.equal(Object.keys(matched.params).length, 2) 105 | }) 106 | 107 | t.test('.match() returns a an object with a route property', function (t) { 108 | t.plan(1) 109 | var r = nanorouter() 110 | r.on('/:foo/:bar', noop) 111 | t.equal(r.match('/baz/qux').route, ':foo/:bar') 112 | }) 113 | }) 114 | --------------------------------------------------------------------------------