├── .editorconfig ├── .gitignore ├── .jshintrc ├── .npmignore ├── LICENSE ├── README.md ├── bower.json ├── examples ├── README.md ├── browser │ ├── .gitignore │ ├── app.js │ ├── index.html │ ├── package.json │ ├── server.js │ ├── views │ │ ├── PetDetail.js │ │ └── PetList.js │ └── webpack.config.js ├── domcache │ ├── .gitignore │ ├── browser.js │ ├── package.json │ ├── router.js │ ├── server.js │ ├── views │ │ ├── App.js │ │ ├── PetDetail.js │ │ └── PetList.js │ ├── webpack.browser.config.js │ └── webpack.server.config.js ├── isomorphic │ ├── .gitignore │ ├── browser.js │ ├── package.json │ ├── router.js │ ├── server.js │ ├── views │ │ ├── App.js │ │ ├── PetDetail.js │ │ └── PetList.js │ ├── webpack.browser.config.js │ └── webpack.server.config.js ├── lazyloadviews │ ├── .gitignore │ ├── browser.js │ ├── package.json │ ├── router.js │ ├── server.js │ ├── views │ │ ├── App.js │ │ ├── PetDetail.js │ │ └── PetList.js │ ├── webpack.browser.config.js │ └── webpack.server.config.js ├── notfound │ ├── .gitignore │ ├── browser.js │ ├── package.json │ ├── router.js │ ├── server.js │ ├── views │ │ ├── App.js │ │ ├── NotFound.js │ │ ├── PetDetail.js │ │ └── PetList.js │ ├── webpack.browser.config.js │ └── webpack.server.config.js ├── preloadermiddleware │ ├── .gitignore │ ├── browser.js │ ├── package.json │ ├── router.js │ ├── server.js │ ├── views │ │ ├── App.js │ │ ├── PetDetail.js │ │ ├── PetList.js │ │ └── Preloader.js │ ├── webpack.browser.config.js │ └── webpack.server.config.js ├── react-frozenhead │ ├── .gitignore │ ├── browser.js │ ├── package.json │ ├── router.js │ ├── server.js │ ├── views │ │ ├── App.js │ │ ├── PetDetail.js │ │ └── PetList.js │ ├── webpack.browser.config.js │ └── webpack.server.config.js ├── react-template │ ├── .gitignore │ ├── browser.js │ ├── package.json │ ├── router.js │ ├── server.js │ ├── templates │ │ ├── App.js │ │ ├── PetDetail.js │ │ └── PetList.js │ ├── webpack.browser.config.js │ └── webpack.server.config.js └── renderinitial │ ├── .gitignore │ ├── browser.js │ ├── package.json │ ├── router.js │ ├── server.js │ ├── views │ ├── App.js │ ├── PetDetail.js │ ├── PetList.js │ └── Preloader.js │ ├── webpack.browser.config.js │ └── webpack.server.config.js ├── gulpfile.js ├── lib ├── LinkHijacker.js ├── Request.js ├── Response.js ├── Route.js ├── Router.js ├── __tests__ │ ├── Request-test.js │ ├── Route-test.js │ └── Router-test.js ├── attach.js ├── errors │ ├── Cancel.js │ ├── Unhandled.js │ └── initError.js ├── history │ ├── BaseHistory.js │ ├── DummyHistory.js │ ├── FallbackHistory.js │ ├── History.js │ ├── PushStateHistory.js │ └── getHistory.js ├── index.js └── utils │ ├── delayed.js │ ├── noop.js │ ├── pathToRegexp.js │ ├── thunkifyAll.js │ └── withoutResults.js ├── package.json └── standalone └── monorouter.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.{md,markdown}] 11 | indent_style = space 12 | indent_size = 4 13 | trim_trailing_whitespace = false 14 | 15 | [*.js] 16 | indent_style = space 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /.tests-built/ 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globalstrict": true, 3 | "browser": true, 4 | "node": true, 5 | "es3": true, 6 | "indent": 2, 7 | "eqnull": true, 8 | "strict": false, 9 | "loopfunc": true, 10 | "globals": { 11 | "ActiveXObject": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /test/ 3 | /vendor/ 4 | /bower.json 5 | /.editorconfig 6 | /.jshintrc 7 | /standalone/ 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 Matthew Dapena-Tretter 2 | http://matthewwithanm.com 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | monorouter 2 | ========== 3 | 4 | monorouter is an isomorphic JavaScript router by [@matthewwithanm] and 5 | [@lettertwo]. It was designed for use with ReactJS but doesn't have any direct 6 | dependencies on it and should be easily adaptable to other virtual DOM 7 | libraries. 8 | 9 | While it can be used for both browser-only and server-only routing, it was 10 | designed from the ground up to be able to route apps on both sides of the wire. 11 | 12 | **Note: This project is in beta and we consider the API in flux. Let us know if 13 | you have any ideas for improvement!** 14 | 15 | 16 | Usage 17 | ----- 18 | 19 | Defining a router looks like this: 20 | 21 | ```javascript 22 | var monorouter = require('monorouter'); 23 | var reactRouting = require('monorouter-react'); 24 | 25 | monorouter() 26 | .setup(reactRouting()) 27 | .route('/', function(req) { 28 | this.render(MyView); 29 | }) 30 | .route('/pets/:name/', function(req) { 31 | this.render(PetView, {petName: req.params.name}); 32 | }); 33 | ``` 34 | 35 | Here, we're simply rendering views for two different routes. With monorouter, a 36 | "view" is any function that returns a DOM descriptor. 37 | 38 | The router can be used on the server with express and [connect-monorouter]: 39 | 40 | ```javascript 41 | var express = require('express'); 42 | var router = require('./path/to/my/router'); 43 | var monorouterMiddleware = require('connect-monorouter'); 44 | 45 | var app = express(); 46 | app.use(monorouterMiddleware(router)); 47 | 48 | var server = app.listen(3000, function() { 49 | console.log('Listening on port %d', server.address().port); 50 | }); 51 | ``` 52 | 53 | And in the browser: 54 | 55 | ```javascript 56 | router 57 | .attach(document) 58 | .captureClicks(); // This is optional—it uses the router to handle normal links. 59 | ``` 60 | 61 | See [the examples][monorouter examples] for a more in-depth look and more 62 | tricks! 63 | 64 | 65 | ### Handling errors 66 | 67 | Error handling with monorouter is similar to [Express]—just add a middleware 68 | with an arity of 3: 69 | 70 | ```javascript 71 | router.use(function(err, req, next) { 72 | if (err.status === 404) { 73 | this.render(My404Template); 74 | } 75 | }); 76 | ``` 77 | 78 | 79 | API 80 | --- 81 | 82 | In addition to the router itself, monorouter has two important objects. The 83 | first one is the request object, and it's passed as the first argument to your 84 | handler. The second is the context (`this`) of your route handler, and it's used 85 | to interface with your application's state (you can think of this as being 86 | similar to a Response object in server-only routers). This section summarizes 87 | their APIs. 88 | 89 | 90 | ### Request 91 | 92 | - **param(name:String)**: Get the value for one of the dynamic parts of your 93 | route: 94 | 95 | ```javascript 96 | monorouter() 97 | .route('/pets/:name', function(req) { 98 | console.log(req.param('name')); 99 | // snip 100 | }); 101 | ``` 102 | 103 | - **params:Object**: A hash of the params used in your route. 104 | - **canceled:Boolean**: A boolean that represents whether the request has been 105 | canceled. This is useful for preventing further action in async callbacks: 106 | 107 | ```javascript 108 | monorouter() 109 | .route('/', function(req) { 110 | http('...', function(err, result) { 111 | if (req.canceled) return; 112 | this.render(MyView, {person: result.people[0]}); 113 | }.bind(this)); 114 | }); 115 | ``` 116 | 117 | Note that it's not necessary to check this value if all you're doing is 118 | rendering since those operations are no-ops when the request has been 119 | canceled. The request is also an EventEmitter that emits a "cancel" event so, 120 | if you'd like to take action immediately when a request is canceled (and abort 121 | an XHR request, for example), you can do that: 122 | 123 | ```javascript 124 | monorouter() 125 | .route('/', function(req) { 126 | var xhr = http('...', function(err, result) { 127 | if (req.canceled) return; 128 | this.render(MyView, {person: result.people[0]}); 129 | }.bind(this)); 130 | this.on('cancel', function() { 131 | xhr.abort(); 132 | }); 133 | }); 134 | ``` 135 | 136 | - **initialOnly:Boolean**: Indicates whether the request is only for the initial 137 | state of the app. `true` when rendering on the server. 138 | - **location:Object**: A parsed version of the requested URL, in a format based 139 | on the [`document.location` interface][document.location]. 140 | - **url:String**: The requested URL. 141 | - **protocol:String**: The protocol of the requested URL, without the colon. 142 | e.g. `"http"` 143 | - **hostname:String**: The hostname of the requested URL, e.g. `'mysite.com'` 144 | - **host:String**: The full host of the requested URL, e.g. `'mysite.com:5000'` 145 | - **search:String**: The search portion of the requested URL, including the 146 | question mark, e.g. `'?hello=5&goodbye=a'` 147 | - **querystring:String**: The search portion of the requested URL, excluding the 148 | question mark, e.g. `'hello=5&goodbye=a'` 149 | - **query:Object**: A version of the query string that's been parsed using 150 | @sindresorhus's [query-string]. 151 | - **hash:String**: The hash portion of the requested URL, including the hash 152 | mark. e.g. `'#this-is-the-hash'` 153 | - **fragment:String**: The hash portion of the requested URL, excluding the hash 154 | mark. e.g. `'this-is-the-hash'` 155 | - **first:Boolean**: Is this the first request being handled by this router? 156 | - **from(causes:String...):Boolean**: Check the cause of the request. Causes 157 | that monorouter sends are: 158 | 159 | * `"startup"` - When a request is triggered by the router initialization in 160 | the browser. 161 | * `"popstate"` - When a request is triggered by a popstate event 162 | * `"link"` - When a request is triggered by a link click captured by the link 163 | hijacker 164 | 165 | You may also send custom causes. For example, [connect-monorouter] uses the 166 | string '"httprequest"'. Generally, causes shouldn't be used to affect routing 167 | behavior—they are meant primarily for logging. 168 | 169 | 170 | ### Handler Context 171 | 172 | Within a route handler, you use properties and methods of `this` to define the 173 | application state. Here are some of those: 174 | 175 | - **render(view:Function?, vars:Object?, callback:Function?)**: Render the view 176 | and consider the request complete. The `view` is a function that returns a 177 | virtual DOM instance. It may be omitted if you've previously set one for this 178 | request using `setView` (e.g. in middleware). "vars" are arguments for this 179 | function that will be bound to it for as long as it's rendered. 180 | - **renderIntermediate(view:Function?, vars:Object?, callback:Function?)**: Like 181 | `render`, but doesn't end the request. This is useful if you'd like to render 182 | several different states during the course of handling a single route. 183 | - **renderInitial(view:Function?, vars:Object?, callback:Function?)**: Like 184 | `render`, but only ends "initialOnly" requests. 185 | - **setView(view:Function)**: Sets the view to be rendered for this response. 186 | The application won't actually be updated until/unless one of the `render*` 187 | methods is called. 188 | - **setVars(vars:Object)**: Add vars for any subsequent renders in this request. 189 | "vars" are passed to the view function for rendering. 190 | - **setState(state:Object)**: "state" is similar to vars in that its values are 191 | passed to the view for rendering. Unlike "vars", however, "state" is preserved 192 | between requests. Setting state also triggers a rerender of the current view. 193 | The state and vars are merged and the result passed to the view function. 194 | - **notFound()**: A function that tells the server to send a 404 status code 195 | with this view. 196 | - **doctype:String**: The doctype for the document. Defaults to the HTML5 197 | doctype. 198 | - **contentType:String**: The content type of the document. Defaults to 199 | `'text/html; charset=utf-8'` 200 | - **beforeRender(hook:Function)**: An interface for adding before-render hooks. 201 | All hooks are executed in parallel immediately prior to rendering. 202 | - **ended:Boolean**: Specifies whether the response has ended. 203 | - **initialEnded:Boolean**: Specifies whether the initial state has been rendered. 204 | 205 | 206 | Philosophy 207 | ---------- 208 | 209 | If the original idea for [react-nested-router] was "[Ember] got it mostly 210 | right," monorouter's can be said to be "[Express] got it mostly right." (Or 211 | [koa]. Or [Flask]. Or [Django]. Or [Rails]…) Server-side routing is very easy: a 212 | request comes in and, in response, you render a template. Each time this 213 | happens, you render the entire document. Until recently, this approach seemed 214 | incongruent with client-side rendering, but ReactJS and the virtual DOM have 215 | changed that. 216 | 217 | Still, client-side routing is fundamentally different than server-side in some 218 | ways—most notably in that state can be shared between routes. monorouter aims to 219 | expose the functionality related to GUI routing that's common to the server and 220 | browser. Some principles of the project are: 221 | 222 | 1. The same routing that's used in the browser should be used to render the 223 | server response. 224 | 2. Routing is (at least potentially) an asynchronous process while rendering a 225 | view is a synchronous one. 226 | 3. Routes need not map to a single state (or view), but may result in any number 227 | of them throughout their life. 228 | * Two of these states are special: the "initial" state (which will be 229 | serialized and sent by the server and which the browser app must begin in) 230 | and the final state (which the app is in once the handling of a route is 231 | completed). 232 | 4. A view is any JavaScript function that returns a DOM descriptor. 233 | 5. Each view should represent the entire document at a given state—not just a 234 | portion of it. 235 | * Not only does this make reasoning about the application easier, it's very 236 | important when dealing with ``s 237 | 6. We think monorouter covers all the possible use cases, and we see it as a 238 | foundation on which to build—both via extensions (i.e. middleware) and 239 | additional abstractions (i.e. JSX-friendly interfaces and declarative, 240 | lifecycle-centric route declarations). 241 | 242 | 243 | 244 | 245 | [@matthewwithanm]: http://github.com/matthewwithanm 246 | [@lettertwo]: http://github.com/lettertwo 247 | [react-nested-router]: https://github.com/rpflorence/react-router 248 | [Ember]: https://github.com/emberjs/ember.js 249 | [Express]: https://github.com/visionmedia/express 250 | [koa]: https://github.com/koajs/koa 251 | [Flask]: https://github.com/mitsuhiko/flask 252 | [Django]: https://github.com/django/django 253 | [Rails]: https://github.com/rails/rails 254 | [react-router-component]: https://github.com/andreypopp/react-router-component 255 | [connect-monorouter]: https://github.com/matthewwithanm/connect-monorouter 256 | [monorouter-react]: https://github.com/matthewwithanm/monorouter-react 257 | [monorouter examples]: https://github.com/matthewwithanm/monorouter/tree/master/examples 258 | [document.location]: https://developer.mozilla.org/en-US/docs/Web/API/document.location 259 | [query-string]: https://github.com/sindresorhus/query-string 260 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorouter", 3 | "version": "0.11.3", 4 | "authors": [ 5 | "Matthew Dapena-Tretter " 6 | ], 7 | "description": "A React Application component", 8 | "main": "./standalone/monorouter.js", 9 | "keywords": [ 10 | "react-component", 11 | "react", 12 | "application", 13 | "router", 14 | "routing", 15 | "component" 16 | ], 17 | "license": "MIT", 18 | "homepage": "https://github.com/matthewwithanm/monorouter", 19 | "ignore": [ 20 | "**/.*", 21 | "node_modules", 22 | "bower_components", 23 | "test", 24 | "tests" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | This directory contains a few examples of using monorouter on the server and 5 | browser. To see them in action: 6 | 7 | 1. `cd` into the directory 8 | 2. Install the example's dependencies (using `npm install`) 9 | 3. Start the example using `npm start` 10 | 11 | This will start a server on port 5000. Go to `http://localhost:5000` in your 12 | browser and see the example in action! 13 | -------------------------------------------------------------------------------- /examples/browser/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /built 3 | -------------------------------------------------------------------------------- /examples/browser/app.js: -------------------------------------------------------------------------------- 1 | var monorouter = require('monorouter'); 2 | var reactRouting = require('monorouter-react'); 3 | var PetList = require('./views/PetList'); 4 | var PetDetail = require('./views/PetDetail'); 5 | 6 | 7 | monorouter() 8 | .setup(reactRouting()) 9 | .route('index', '/', function(req) { 10 | this.render(PetList); 11 | }) 12 | .route('pet', '/pet/:name', function(req) { 13 | this.render(PetDetail, {petName: req.params.name}); 14 | }) 15 | .attach(document.body) 16 | .captureClicks(); 17 | -------------------------------------------------------------------------------- /examples/browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Browser Example 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorouter-browser-example", 3 | "version": "1.0.0", 4 | "description": "An example of client-side routing with monorouter", 5 | "main": "server.js", 6 | "scripts": { 7 | "build": "webpack --config webpack.config.js", 8 | "serve": "node ./server.js", 9 | "start": "npm run build && npm run serve" 10 | }, 11 | "author": "Eric Eldredge ", 12 | "license": "MIT", 13 | "dependencies": { 14 | "monorouter": "^0.7.1", 15 | "react": "^0.11.1", 16 | "monorouter-react": "^0.2.0", 17 | "express": "^4.7.2", 18 | "webpack": "^1.3.2-beta9", 19 | "jsx-loader": "~0.11.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/browser/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var path = require('path'); 3 | 4 | port = process.env.PORT || 5000; 5 | 6 | express() 7 | .get('/app.js', function(req, res) { 8 | res.sendfile(path.join(__dirname, 'built', 'app.js')); 9 | }) 10 | .get('/', function(req, res) { 11 | res.sendfile(path.join(__dirname, 'index.html')); 12 | }) 13 | .listen(port, function() { 14 | console.log("Listening on " + port + "."); 15 | console.log("Go to in your browser."); 16 | }); 17 | -------------------------------------------------------------------------------- /examples/browser/views/PetDetail.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | 4 | function PetDetail(props) { 5 | return ( 6 |
7 |

{props.petName}

8 | See all pets! 9 |
10 | ); 11 | } 12 | 13 | 14 | module.exports = PetDetail; 15 | -------------------------------------------------------------------------------- /examples/browser/views/PetList.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | 4 | var pets = [ 5 | {name: 'Winston'}, 6 | {name: 'Chaplin'}, 7 | {name: 'Bennie'} 8 | ]; 9 | 10 | function PetList() { 11 | var links = pets.map(function(pet) { 12 | return
  • {pet.name}
  • ; 13 | }); 14 | return ( 15 |
      16 | {links} 17 |
    18 | ); 19 | } 20 | 21 | module.exports = PetList; 22 | -------------------------------------------------------------------------------- /examples/browser/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | context: __dirname, 5 | entry: { 6 | app: './app.js' 7 | }, 8 | 9 | output: { 10 | filename: '[name].js', 11 | path: path.join(__dirname, 'built'), 12 | devtool: '$@inline-source-map', 13 | libraryTarget: 'umd', 14 | publicPath: '../built/' 15 | }, 16 | 17 | module: { 18 | loaders: [ 19 | {test: /\.js$/, loader: 'jsx'} 20 | ] 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /examples/domcache/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /built 3 | -------------------------------------------------------------------------------- /examples/domcache/browser.js: -------------------------------------------------------------------------------- 1 | var Router = require('./router'); 2 | 3 | Router.attach(document).captureClicks(); 4 | -------------------------------------------------------------------------------- /examples/domcache/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorouter-domcache-example", 3 | "version": "1.0.0", 4 | "description": "An example of caching data in the DOM with monorouter", 5 | "main": "server.js", 6 | "scripts": { 7 | "build": "webpack --config webpack.browser.config.js && webpack --config webpack.server.config.js", 8 | "serve": "node ./built/server.js", 9 | "start": "npm run build && npm run serve" 10 | }, 11 | "author": "Eric Eldredge ", 12 | "license": "MIT", 13 | "dependencies": { 14 | "monorouter": "^0.7.1", 15 | "react": "^0.11.1", 16 | "monorouter-react": "^0.2.0", 17 | "connect-monorouter": "^0.2.1", 18 | "express": "^4.7.2", 19 | "webpack": "^1.3.2-beta9", 20 | "jsx-loader": "~0.11.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/domcache/router.js: -------------------------------------------------------------------------------- 1 | var monorouter = require('monorouter'); 2 | var reactRouting = require('monorouter-react'); 3 | var PetList = require('./views/PetList'); 4 | var PetDetail = require('./views/PetDetail'); 5 | 6 | var pets = [ 7 | {name: 'Winston'}, 8 | {name: 'Chaplin'}, 9 | {name: 'Bennie'} 10 | ]; 11 | 12 | function fetchPetList (cb) { 13 | // Artificially make the response take a while. 14 | setTimeout(function() { 15 | cb(null, pets); 16 | }, Math.round(200 + Math.random() * 800)); 17 | } 18 | 19 | function findPetName (petName, data) { 20 | for (var i = 0; i < pets.length; i++) { 21 | var pet = data[i]; 22 | if (pet.name.toLowerCase() === petName) return pet.name; 23 | } 24 | } 25 | 26 | module.exports = monorouter() 27 | .setup(reactRouting()) 28 | .route('index', '/', function(req) { 29 | this.domCache('petData', fetchPetList, function (err, data) { 30 | this.render(PetList, {pets: data}); 31 | }); 32 | }) 33 | .route('pet', '/pet/:name', function(req) { 34 | this.domCache('petData', fetchPetList, function (err, data) { 35 | this.render(PetDetail, {petName: findPetName(req.params.name, data)}); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /examples/domcache/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var routerMiddleware = require('connect-monorouter'); 3 | var Router = require('./router'); 4 | var path = require('path'); 5 | 6 | port = process.env.PORT || 5000; 7 | 8 | express() 9 | .use(routerMiddleware(Router)) 10 | .get('/browser.js', function(req, res) { 11 | res.sendfile(path.join(__dirname, 'browser.js')); 12 | }) 13 | .listen(port, function() { 14 | console.log("Listening on " + port + "."); 15 | console.log("Go to in your browser."); 16 | }); 17 | -------------------------------------------------------------------------------- /examples/domcache/views/App.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | 4 | function App(props, children) { 5 | return ( 6 | 7 | 8 | Domcache Example 9 | 10 | 11 | {children} 12 | {props.domCache()} 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | 20 | module.exports = App; 21 | -------------------------------------------------------------------------------- /examples/domcache/views/PetDetail.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | var App = require('./App'); 4 | 5 | function PetDetail(props) { 6 | return ( 7 | 8 |
    9 |

    {props.petName}

    10 | See all pets! 11 |
    12 |
    13 | ); 14 | } 15 | 16 | 17 | module.exports = PetDetail; 18 | -------------------------------------------------------------------------------- /examples/domcache/views/PetList.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | var App = require('./App'); 4 | 5 | function PetList(props) { 6 | var links = props.pets.map(function(pet) { 7 | return
  • {pet.name}
  • ; 8 | }); 9 | return ( 10 | 11 |
      12 | {links} 13 |
    14 |
    15 | ); 16 | } 17 | 18 | module.exports = PetList; 19 | -------------------------------------------------------------------------------- /examples/domcache/webpack.browser.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | context: __dirname, 5 | entry: { 6 | browser: './browser.js' 7 | }, 8 | 9 | output: { 10 | filename: '[name].js', 11 | path: path.join(__dirname, 'built'), 12 | devtool: '$@inline-source-map', 13 | libraryTarget: 'umd', 14 | publicPath: '../built/' 15 | }, 16 | 17 | module: { 18 | loaders: [ 19 | {test: /\.js$/, loader: 'jsx'} 20 | ] 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /examples/domcache/webpack.server.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | context: __dirname, 5 | entry: { 6 | server: './server.js' 7 | }, 8 | externals: ['express', 'react'], 9 | target: 'node', 10 | node: { 11 | console: false, 12 | process: false, 13 | global: false, 14 | buffer: false, 15 | __filename: false, 16 | __dirname: false 17 | }, 18 | 19 | output: { 20 | filename: '[name].js', 21 | path: path.join(__dirname, 'built'), 22 | devtool: '$@inline-source-map', 23 | libraryTarget: 'commonjs2', 24 | publicPath: '../built/' 25 | }, 26 | 27 | module: { 28 | loaders: [ 29 | {test: /\.js$/, loader: 'jsx'} 30 | ] 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /examples/isomorphic/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /built 3 | -------------------------------------------------------------------------------- /examples/isomorphic/browser.js: -------------------------------------------------------------------------------- 1 | var Router = require('./router'); 2 | 3 | Router.attach(document).captureClicks(); 4 | -------------------------------------------------------------------------------- /examples/isomorphic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorouter-isomorphic-example", 3 | "version": "1.0.0", 4 | "description": "An example of server- and client-side routing with monorouter", 5 | "main": "server.js", 6 | "scripts": { 7 | "build": "webpack --config webpack.browser.config.js && webpack --config webpack.server.config.js", 8 | "serve": "node ./built/server.js", 9 | "start": "npm run build && npm run serve" 10 | }, 11 | "author": "Eric Eldredge ", 12 | "license": "MIT", 13 | "dependencies": { 14 | "monorouter": "^0.7.1", 15 | "react": "^0.11.1", 16 | "monorouter-react": "^0.2.0", 17 | "connect-monorouter": "^0.2.1", 18 | "express": "^4.7.2", 19 | "webpack": "^1.3.2-beta9", 20 | "jsx-loader": "~0.11.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/isomorphic/router.js: -------------------------------------------------------------------------------- 1 | var monorouter = require('monorouter'); 2 | var reactRouting = require('monorouter-react'); 3 | var PetList = require('./views/PetList'); 4 | var PetDetail = require('./views/PetDetail'); 5 | 6 | 7 | module.exports = monorouter() 8 | .setup(reactRouting()) 9 | .route('index', '/', function(req) { 10 | this.render(PetList); 11 | }) 12 | .route('pet', '/pet/:name', function(req) { 13 | this.render(PetDetail, {petName: req.params.name}); 14 | }); 15 | -------------------------------------------------------------------------------- /examples/isomorphic/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var routerMiddleware = require('connect-monorouter'); 3 | var Router = require('./router'); 4 | var path = require('path'); 5 | 6 | port = process.env.PORT || 5000; 7 | 8 | express() 9 | .use(routerMiddleware(Router)) 10 | .get('/browser.js', function(req, res) { 11 | res.sendfile(path.join(__dirname, 'browser.js')); 12 | }) 13 | .listen(port, function() { 14 | console.log("Listening on " + port + "."); 15 | console.log("Go to in your browser."); 16 | }); 17 | -------------------------------------------------------------------------------- /examples/isomorphic/views/App.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | 4 | function App(props, children) { 5 | return ( 6 | 7 | 8 | Isomorphic Example 9 | 10 | 11 | {children} 12 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | 19 | module.exports = App; 20 | -------------------------------------------------------------------------------- /examples/isomorphic/views/PetDetail.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | var App = require('./App'); 4 | 5 | function PetDetail(props) { 6 | return ( 7 | 8 |
    9 |

    {props.petName}

    10 | See all pets! 11 |
    12 |
    13 | ); 14 | } 15 | 16 | 17 | module.exports = PetDetail; 18 | -------------------------------------------------------------------------------- /examples/isomorphic/views/PetList.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | var App = require('./App'); 4 | 5 | var pets = [ 6 | {name: 'Winston'}, 7 | {name: 'Chaplin'}, 8 | {name: 'Bennie'} 9 | ]; 10 | 11 | function PetList() { 12 | var links = pets.map(function(pet) { 13 | return
  • {pet.name}
  • ; 14 | }); 15 | return ( 16 | 17 |
      18 | {links} 19 |
    20 |
    21 | ); 22 | } 23 | 24 | module.exports = PetList; 25 | -------------------------------------------------------------------------------- /examples/isomorphic/webpack.browser.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | context: __dirname, 5 | entry: { 6 | browser: './browser.js' 7 | }, 8 | 9 | output: { 10 | filename: '[name].js', 11 | path: path.join(__dirname, 'built'), 12 | devtool: '$@inline-source-map', 13 | libraryTarget: 'umd', 14 | publicPath: '../built/' 15 | }, 16 | 17 | module: { 18 | loaders: [ 19 | {test: /\.js$/, loader: 'jsx'} 20 | ] 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /examples/isomorphic/webpack.server.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | context: __dirname, 5 | entry: { 6 | server: './server.js' 7 | }, 8 | externals: ['express', 'react'], 9 | target: 'node', 10 | node: { 11 | console: false, 12 | process: false, 13 | global: false, 14 | buffer: false, 15 | __filename: false, 16 | __dirname: false 17 | }, 18 | 19 | output: { 20 | filename: '[name].js', 21 | path: path.join(__dirname, 'built'), 22 | devtool: '$@inline-source-map', 23 | libraryTarget: 'commonjs2', 24 | publicPath: '../built/' 25 | }, 26 | 27 | module: { 28 | loaders: [ 29 | {test: /\.js$/, loader: 'jsx'} 30 | ] 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /examples/lazyloadviews/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /built 3 | -------------------------------------------------------------------------------- /examples/lazyloadviews/browser.js: -------------------------------------------------------------------------------- 1 | var Router = require('./router'); 2 | 3 | Router.attach(document).captureClicks(); 4 | -------------------------------------------------------------------------------- /examples/lazyloadviews/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorouter-lazyloadviews-example", 3 | "version": "1.0.0", 4 | "description": "An example of lazily loaded views with monorouter", 5 | "main": "server.js", 6 | "scripts": { 7 | "build": "webpack --config webpack.browser.config.js && webpack --config webpack.server.config.js", 8 | "serve": "node ./built/server.js", 9 | "start": "npm run build && npm run serve" 10 | }, 11 | "author": "Eric Eldredge ", 12 | "license": "MIT", 13 | "dependencies": { 14 | "monorouter": "^0.7.1", 15 | "react": "^0.11.1", 16 | "monorouter-react": "^0.2.0", 17 | "connect-monorouter": "^0.2.1", 18 | "express": "^4.7.2", 19 | "webpack": "^1.3.2-beta9", 20 | "jsx-loader": "~0.11.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/lazyloadviews/router.js: -------------------------------------------------------------------------------- 1 | var monorouter = require('monorouter'); 2 | var reactRouting = require('monorouter-react'); 3 | 4 | var pets = [ 5 | {name: 'Winston'}, 6 | {name: 'Chaplin'}, 7 | {name: 'Bennie'} 8 | ]; 9 | 10 | function findPetName (petName, data) { 11 | for (var i = 0; i < pets.length; i++) { 12 | var pet = data[i]; 13 | if (pet.name.toLowerCase() === petName) return pet.name; 14 | } 15 | } 16 | 17 | module.exports = monorouter() 18 | .setup(reactRouting()) 19 | .route('index', '/', function(req) { 20 | require.ensure(['./views/PetList'], function(require) { 21 | this.render(require('./views/PetList'), {pets: pets}); 22 | }.bind(this)); 23 | }) 24 | .route('pet', '/pet/:name', function(req) { 25 | require.ensure(['./views/PetDetail'], function(require) { 26 | this.render( 27 | require('./views/PetDetail'), 28 | {petName: findPetName(req.params.name, pets)} 29 | ); 30 | }.bind(this)); 31 | }); 32 | -------------------------------------------------------------------------------- /examples/lazyloadviews/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var routerMiddleware = require('connect-monorouter'); 3 | var Router = require('./router'); 4 | var path = require('path'); 5 | 6 | port = process.env.PORT || 5000; 7 | 8 | express() 9 | .use('/assets', express.static(__dirname + '/assets')) 10 | .use(routerMiddleware(Router)) 11 | .listen(port, function() { 12 | console.log("Listening on " + port + "."); 13 | console.log("Go to in your browser."); 14 | }); 15 | -------------------------------------------------------------------------------- /examples/lazyloadviews/views/App.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | 4 | function App(props, children) { 5 | return ( 6 | 7 | 8 | Lazy Load Views Example 9 | 10 | 11 | {children} 12 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | 19 | module.exports = App; 20 | -------------------------------------------------------------------------------- /examples/lazyloadviews/views/PetDetail.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | var App = require('./App'); 4 | 5 | function PetDetail(props) { 6 | return ( 7 | 8 |
    9 |

    {props.petName}

    10 | See all pets! 11 |
    12 |
    13 | ); 14 | } 15 | 16 | 17 | module.exports = PetDetail; 18 | -------------------------------------------------------------------------------- /examples/lazyloadviews/views/PetList.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | var App = require('./App'); 4 | 5 | function PetList(props) { 6 | var links = props.pets.map(function(pet) { 7 | return
  • {pet.name}
  • ; 8 | }); 9 | return ( 10 | 11 |
      12 | {links} 13 |
    14 |
    15 | ); 16 | } 17 | 18 | module.exports = PetList; 19 | -------------------------------------------------------------------------------- /examples/lazyloadviews/webpack.browser.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | context: __dirname, 5 | entry: { 6 | browser: './browser.js' 7 | }, 8 | 9 | output: { 10 | filename: '[name].js', 11 | path: path.join(__dirname, 'built', 'assets'), 12 | devtool: '$@inline-source-map', 13 | libraryTarget: 'umd', 14 | publicPath: '/assets/' 15 | }, 16 | 17 | module: { 18 | loaders: [ 19 | {test: /\.js$/, loader: 'jsx'} 20 | ] 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /examples/lazyloadviews/webpack.server.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | context: __dirname, 5 | entry: { 6 | server: './server.js' 7 | }, 8 | externals: ['express', 'react'], 9 | target: 'node', 10 | node: { 11 | console: false, 12 | process: false, 13 | global: false, 14 | buffer: false, 15 | __filename: false, 16 | __dirname: false 17 | }, 18 | 19 | output: { 20 | filename: 'server.js', 21 | path: path.join(__dirname, 'built'), 22 | devtool: '$@inline-source-map', 23 | libraryTarget: 'commonjs2' 24 | }, 25 | 26 | module: { 27 | loaders: [ 28 | {test: /\.js$/, loader: 'jsx'} 29 | ] 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /examples/notfound/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /built 3 | -------------------------------------------------------------------------------- /examples/notfound/browser.js: -------------------------------------------------------------------------------- 1 | var Router = require('./router'); 2 | 3 | Router.attach(document).captureClicks(); 4 | -------------------------------------------------------------------------------- /examples/notfound/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorouter-notfound-example", 3 | "version": "1.0.0", 4 | "description": "An example of 'not found' (404) handling with monorouter", 5 | "main": "server.js", 6 | "scripts": { 7 | "build": "webpack --config webpack.browser.config.js && webpack --config webpack.server.config.js", 8 | "serve": "node ./built/server.js", 9 | "start": "npm run build && npm run serve" 10 | }, 11 | "author": "Eric Eldredge ", 12 | "license": "MIT", 13 | "dependencies": { 14 | "monorouter": "^0.7.1", 15 | "react": "^0.11.1", 16 | "monorouter-react": "^0.2.0", 17 | "connect-monorouter": "^0.2.1", 18 | "express": "^4.7.2", 19 | "webpack": "^1.3.2-beta9", 20 | "jsx-loader": "~0.11.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/notfound/router.js: -------------------------------------------------------------------------------- 1 | var monorouter = require('monorouter'); 2 | var reactRouting = require('monorouter-react'); 3 | var PetList = require('./views/PetList'); 4 | var PetDetail = require('./views/PetDetail'); 5 | var NotFound = require('./views/NotFound'); 6 | 7 | var pets = [ 8 | {name: 'Winston'}, 9 | {name: 'Chaplin'}, 10 | {name: 'Bennie'} 11 | ]; 12 | 13 | function findPetName (petName, data) { 14 | for (var i = 0; i < pets.length; i++) { 15 | var pet = data[i]; 16 | if (pet.name.toLowerCase() === petName) return pet.name; 17 | } 18 | } 19 | 20 | module.exports = monorouter() 21 | .setup(reactRouting()) 22 | .route('index', '/', function(req) { 23 | this.render(PetList, {pets: pets}); 24 | }) 25 | .route('pet', '/pet/:name', function(req) { 26 | var petName = findPetName(req.params.name, pets); 27 | if (!petName) { 28 | return this 29 | .notFound() 30 | .render(NotFound, {msg: "No pet " + req.params.name + " exists!"}); 31 | } 32 | this.render(PetDetail, {petName: petName}); 33 | }); 34 | -------------------------------------------------------------------------------- /examples/notfound/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var routerMiddleware = require('connect-monorouter'); 3 | var Router = require('./router'); 4 | var path = require('path'); 5 | 6 | port = process.env.PORT || 5000; 7 | 8 | express() 9 | .use(routerMiddleware(Router)) 10 | .get('/browser.js', function(req, res) { 11 | res.sendfile(path.join(__dirname, 'browser.js')); 12 | }) 13 | .listen(port, function() { 14 | console.log("Listening on " + port + "."); 15 | console.log("Go to in your browser."); 16 | }); 17 | -------------------------------------------------------------------------------- /examples/notfound/views/App.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | 4 | function App(props, children) { 5 | return ( 6 | 7 | 8 | NotFound Example 9 | 10 | 11 | {children} 12 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | 19 | module.exports = App; 20 | -------------------------------------------------------------------------------- /examples/notfound/views/NotFound.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | var App = require('./App'); 4 | 5 | function NotFound(props) { 6 | return ( 7 | 8 |
    9 |

    Not Found

    10 |

    {props.msg}

    11 |
    12 |
    13 | ); 14 | } 15 | 16 | 17 | module.exports = NotFound; 18 | -------------------------------------------------------------------------------- /examples/notfound/views/PetDetail.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | var App = require('./App'); 4 | 5 | function PetDetail(props) { 6 | return ( 7 | 8 |
    9 |

    {props.petName}

    10 | See all pets! 11 |
    12 |
    13 | ); 14 | } 15 | 16 | 17 | module.exports = PetDetail; 18 | -------------------------------------------------------------------------------- /examples/notfound/views/PetList.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | var App = require('./App'); 4 | 5 | function PetList(props) { 6 | var links = props.pets 7 | .map(function(pet) { 8 | return
  • {pet.name}
  • ; 9 | }) 10 | .concat([
  • fake
  • ]); 11 | return ( 12 | 13 |
      14 | {links} 15 |
    16 |
    17 | ); 18 | } 19 | 20 | module.exports = PetList; 21 | -------------------------------------------------------------------------------- /examples/notfound/webpack.browser.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | context: __dirname, 5 | entry: { 6 | browser: './browser.js' 7 | }, 8 | 9 | output: { 10 | filename: '[name].js', 11 | path: path.join(__dirname, 'built'), 12 | devtool: '$@inline-source-map', 13 | libraryTarget: 'umd', 14 | publicPath: '../built/' 15 | }, 16 | 17 | module: { 18 | loaders: [ 19 | {test: /\.js$/, loader: 'jsx'} 20 | ] 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /examples/notfound/webpack.server.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | context: __dirname, 5 | entry: { 6 | server: './server.js' 7 | }, 8 | externals: ['express', 'react'], 9 | target: 'node', 10 | node: { 11 | console: false, 12 | process: false, 13 | global: false, 14 | buffer: false, 15 | __filename: false, 16 | __dirname: false 17 | }, 18 | 19 | output: { 20 | filename: '[name].js', 21 | path: path.join(__dirname, 'built'), 22 | devtool: '$@inline-source-map', 23 | libraryTarget: 'commonjs2', 24 | publicPath: '../built/' 25 | }, 26 | 27 | module: { 28 | loaders: [ 29 | {test: /\.js$/, loader: 'jsx'} 30 | ] 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /examples/preloadermiddleware/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /built 3 | -------------------------------------------------------------------------------- /examples/preloadermiddleware/browser.js: -------------------------------------------------------------------------------- 1 | var Router = require('./router'); 2 | 3 | Router.attach(document).captureClicks(); 4 | -------------------------------------------------------------------------------- /examples/preloadermiddleware/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorouter-preloadermiddleware-example", 3 | "version": "1.0.0", 4 | "description": "An example of using middleware with monorouter", 5 | "main": "server.js", 6 | "scripts": { 7 | "build": "webpack --config webpack.browser.config.js && webpack --config webpack.server.config.js", 8 | "serve": "node ./built/server.js", 9 | "start": "npm run build && npm run serve" 10 | }, 11 | "author": "Eric Eldredge ", 12 | "license": "MIT", 13 | "dependencies": { 14 | "monorouter": "^0.7.1", 15 | "react": "^0.11.1", 16 | "monorouter-react": "^0.2.0", 17 | "connect-monorouter": "^0.2.1", 18 | "express": "^4.7.2", 19 | "webpack": "^1.3.2-beta9", 20 | "jsx-loader": "~0.11.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/preloadermiddleware/router.js: -------------------------------------------------------------------------------- 1 | var monorouter = require('monorouter'); 2 | var reactRouting = require('monorouter-react'); 3 | var PetList = require('./views/PetList'); 4 | var PetDetail = require('./views/PetDetail'); 5 | var Preloader = require('./views/Preloader'); 6 | 7 | var pets = [ 8 | {name: 'Winston'}, 9 | {name: 'Chaplin'}, 10 | {name: 'Bennie'} 11 | ]; 12 | 13 | function fetchPetList (cb) { 14 | // Artificially make the response take a while. 15 | setTimeout(function() { 16 | cb(null, pets); 17 | }, Math.round(200 + Math.random() * 800)); 18 | } 19 | 20 | function findPetName (petName, data) { 21 | for (var i = 0; i < pets.length; i++) { 22 | var pet = data[i]; 23 | if (pet.name.toLowerCase() === petName) return pet.name; 24 | } 25 | } 26 | 27 | function preloadData (req, next) { 28 | this.renderInitial(Preloader, function() { 29 | fetchPetList(function(err, data) { 30 | var viewProps = {pets: data}; 31 | if (req.params && req.params.name) 32 | viewProps.petName = findPetName(req.params.name, data); 33 | this.setVars(viewProps); 34 | next(); 35 | }.bind(this)); 36 | }.bind(this)); 37 | } 38 | 39 | module.exports = monorouter() 40 | .setup(reactRouting()) 41 | .route('index', '/', preloadData, function(req) { 42 | this.render(PetList); 43 | }) 44 | .route('pet', '/pet/:name', preloadData, function(req) { 45 | // NOTE: Because renderPreloader renders an initial view, we lose the 46 | // opportunity to have the server send a 404, and the client will have to 47 | // display a 'missing' view. If you want the server to send 404s, you have 48 | // to call `this.unhandled()` before `this.renderInitial()`. 49 | this.render(PetDetail); 50 | }); 51 | -------------------------------------------------------------------------------- /examples/preloadermiddleware/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var routerMiddleware = require('connect-monorouter'); 3 | var Router = require('./router'); 4 | var path = require('path'); 5 | 6 | port = process.env.PORT || 5000; 7 | 8 | express() 9 | .use(routerMiddleware(Router)) 10 | .get('/browser.js', function(req, res) { 11 | res.sendfile(path.join(__dirname, 'browser.js')); 12 | }) 13 | .listen(port, function() { 14 | console.log("Listening on " + port + "."); 15 | console.log("Go to in your browser."); 16 | }); 17 | -------------------------------------------------------------------------------- /examples/preloadermiddleware/views/App.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | 4 | function App(props, children) { 5 | return ( 6 | 7 | 8 | Preloader Middleware Example 9 | 10 | 11 | {children} 12 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | 19 | module.exports = App; 20 | -------------------------------------------------------------------------------- /examples/preloadermiddleware/views/PetDetail.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | var App = require('./App'); 4 | 5 | function PetDetail(props) { 6 | return ( 7 | 8 |
    9 |

    {props.petName}

    10 | See all pets! 11 |
    12 |
    13 | ); 14 | } 15 | 16 | 17 | module.exports = PetDetail; 18 | -------------------------------------------------------------------------------- /examples/preloadermiddleware/views/PetList.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | var App = require('./App'); 4 | 5 | function PetList(props) { 6 | var links = props.pets.map(function(pet) { 7 | return
  • {pet.name}
  • ; 8 | }); 9 | return ( 10 | 11 |
      12 | {links} 13 |
    14 |
    15 | ); 16 | } 17 | 18 | module.exports = PetList; 19 | -------------------------------------------------------------------------------- /examples/preloadermiddleware/views/Preloader.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | var App = require('./App'); 4 | 5 | function Preloader(props) { 6 | return ( 7 | 8 |
    9 |

    Loading...

    10 |
    11 |
    12 | ); 13 | } 14 | 15 | module.exports = Preloader; 16 | -------------------------------------------------------------------------------- /examples/preloadermiddleware/webpack.browser.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | context: __dirname, 5 | entry: { 6 | browser: './browser.js' 7 | }, 8 | 9 | output: { 10 | filename: '[name].js', 11 | path: path.join(__dirname, 'built'), 12 | devtool: '$@inline-source-map', 13 | libraryTarget: 'umd', 14 | publicPath: '../built/' 15 | }, 16 | 17 | module: { 18 | loaders: [ 19 | {test: /\.js$/, loader: 'jsx'} 20 | ] 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /examples/preloadermiddleware/webpack.server.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | context: __dirname, 5 | entry: { 6 | server: './server.js' 7 | }, 8 | externals: ['express', 'react'], 9 | target: 'node', 10 | node: { 11 | console: false, 12 | process: false, 13 | global: false, 14 | buffer: false, 15 | __filename: false, 16 | __dirname: false 17 | }, 18 | 19 | output: { 20 | filename: '[name].js', 21 | path: path.join(__dirname, 'built'), 22 | devtool: '$@inline-source-map', 23 | libraryTarget: 'commonjs2', 24 | publicPath: '../built/' 25 | }, 26 | 27 | module: { 28 | loaders: [ 29 | {test: /\.js$/, loader: 'jsx'} 30 | ] 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /examples/react-frozenhead/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /built 3 | -------------------------------------------------------------------------------- /examples/react-frozenhead/browser.js: -------------------------------------------------------------------------------- 1 | var Router = require('./router'); 2 | 3 | Router.attach(document).captureClicks(); 4 | -------------------------------------------------------------------------------- /examples/react-frozenhead/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorouter-react-frozenhead-example", 3 | "version": "1.0.0", 4 | "description": "An example of using react-frozenhead with monorouter", 5 | "main": "server.js", 6 | "scripts": { 7 | "build": "webpack --config webpack.browser.config.js && webpack --config webpack.server.config.js", 8 | "serve": "node ./built/server.js", 9 | "start": "npm run build && npm run serve" 10 | }, 11 | "author": "Eric Eldredge ", 12 | "license": "MIT", 13 | "dependencies": { 14 | "monorouter": "^0.7.1", 15 | "react": "^0.11.1", 16 | "monorouter-react": "^0.2.0", 17 | "connect-monorouter": "^0.2.1", 18 | "express": "^4.7.2", 19 | "webpack": "^1.3.2-beta9", 20 | "react-frozenhead": "^0.1.0", 21 | "jsx-loader": "~0.11.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/react-frozenhead/router.js: -------------------------------------------------------------------------------- 1 | var monorouter = require('monorouter'); 2 | var reactRouting = require('monorouter-react'); 3 | var PetList = require('./views/PetList'); 4 | var PetDetail = require('./views/PetDetail'); 5 | 6 | 7 | module.exports = monorouter() 8 | .setup(reactRouting()) 9 | .route('index', '/', function(req) { 10 | this.render(PetList); 11 | }) 12 | .route('pet', '/pet/:name', function(req) { 13 | this.render(PetDetail, {petName: req.params.name}); 14 | }); 15 | -------------------------------------------------------------------------------- /examples/react-frozenhead/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var routerMiddleware = require('connect-monorouter'); 3 | var Router = require('./router'); 4 | var path = require('path'); 5 | 6 | port = process.env.PORT || 5000; 7 | 8 | express() 9 | .use(routerMiddleware(Router)) 10 | .get('/browser.js', function(req, res) { 11 | res.sendfile(path.join(__dirname, 'browser.js')); 12 | }) 13 | .listen(port, function() { 14 | console.log("Listening on " + port + "."); 15 | console.log("Go to in your browser."); 16 | }); 17 | -------------------------------------------------------------------------------- /examples/react-frozenhead/views/App.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | var head = require('react-frozenhead'); 4 | 5 | function App(props, children) { 6 | return ( 7 | 8 | 9 | {props.title + " :: React Frozenhead Example"} 10 | {props.headExtras} 11 | 12 | 13 | {children} 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | 21 | module.exports = App; 22 | -------------------------------------------------------------------------------- /examples/react-frozenhead/views/PetDetail.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | var App = require('./App'); 4 | 5 | function PetDetail(props) { 6 | var metaTags = [ 7 | , 8 | 9 | ]; 10 | return ( 11 | 12 |
    13 |

    {props.petName}

    14 | See all pets! 15 |
    16 |
    17 | ); 18 | } 19 | 20 | 21 | module.exports = PetDetail; 22 | -------------------------------------------------------------------------------- /examples/react-frozenhead/views/PetList.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | var App = require('./App'); 4 | 5 | var pets = [ 6 | {name: 'Winston'}, 7 | {name: 'Chaplin'}, 8 | {name: 'Bennie'} 9 | ]; 10 | 11 | function PetList() { 12 | var links = pets.map(function(pet) { 13 | return
  • {pet.name}
  • ; 14 | }); 15 | var metaTags = [ 16 | , 17 | 18 | ]; 19 | return ( 20 | 21 |
      22 | {links} 23 |
    24 |
    25 | ); 26 | } 27 | 28 | module.exports = PetList; 29 | -------------------------------------------------------------------------------- /examples/react-frozenhead/webpack.browser.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | context: __dirname, 5 | entry: { 6 | browser: './browser.js' 7 | }, 8 | 9 | output: { 10 | filename: '[name].js', 11 | path: path.join(__dirname, 'built'), 12 | devtool: '$@inline-source-map', 13 | libraryTarget: 'umd', 14 | publicPath: '../built/' 15 | }, 16 | 17 | module: { 18 | loaders: [ 19 | {test: /\.js$/, loader: 'jsx'} 20 | ] 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /examples/react-frozenhead/webpack.server.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | context: __dirname, 5 | entry: { 6 | server: './server.js' 7 | }, 8 | externals: ['express', 'react'], 9 | target: 'node', 10 | node: { 11 | console: false, 12 | process: false, 13 | global: false, 14 | buffer: false, 15 | __filename: false, 16 | __dirname: false 17 | }, 18 | 19 | output: { 20 | filename: '[name].js', 21 | path: path.join(__dirname, 'built'), 22 | devtool: '$@inline-source-map', 23 | libraryTarget: 'commonjs2', 24 | publicPath: '../built/' 25 | }, 26 | 27 | module: { 28 | loaders: [ 29 | {test: /\.js$/, loader: 'jsx'} 30 | ] 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /examples/react-template/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /built 3 | -------------------------------------------------------------------------------- /examples/react-template/browser.js: -------------------------------------------------------------------------------- 1 | var Router = require('./router'); 2 | 3 | Router.attach(document).captureClicks(); 4 | -------------------------------------------------------------------------------- /examples/react-template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorouter-react-template-example", 3 | "version": "1.0.0", 4 | "description": "An example of using react-template with monorouter", 5 | "main": "server.js", 6 | "scripts": { 7 | "build": "webpack --config webpack.browser.config.js && webpack --config webpack.server.config.js", 8 | "serve": "node ./built/server.js", 9 | "start": "npm run build && npm run serve" 10 | }, 11 | "author": "Eric Eldredge ", 12 | "license": "MIT", 13 | "dependencies": { 14 | "monorouter": "^0.7.1", 15 | "react": "^0.11.1", 16 | "monorouter-react": "^0.2.0", 17 | "connect-monorouter": "^0.2.1", 18 | "express": "^4.7.2", 19 | "webpack": "^1.3.2-beta9", 20 | "react-template": "^0.3.1", 21 | "jsx-loader": "~0.11.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/react-template/router.js: -------------------------------------------------------------------------------- 1 | var monorouter = require('monorouter'); 2 | var reactRouting = require('monorouter-react'); 3 | var PetList = require('./templates/PetList'); 4 | var PetDetail = require('./templates/PetDetail'); 5 | 6 | 7 | module.exports = monorouter() 8 | .setup(reactRouting()) 9 | .route('index', '/', function(req) { 10 | this.render(PetList); 11 | }) 12 | .route('pet', '/pet/:name', function(req) { 13 | this.render(PetDetail, {petName: req.params.name}); 14 | }); 15 | -------------------------------------------------------------------------------- /examples/react-template/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var routerMiddleware = require('connect-monorouter'); 3 | var Router = require('./router'); 4 | var path = require('path'); 5 | 6 | port = process.env.PORT || 5000; 7 | 8 | express() 9 | .use(routerMiddleware(Router)) 10 | .get('/browser.js', function(req, res) { 11 | res.sendfile(path.join(__dirname, 'browser.js')); 12 | }) 13 | .listen(port, function() { 14 | console.log("Listening on " + port + "."); 15 | console.log("Go to in your browser."); 16 | }); 17 | -------------------------------------------------------------------------------- /examples/react-template/templates/App.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | var ReactTemplate = require('react-template'); 4 | 5 | 6 | var App = ReactTemplate.create({ 7 | render: function() { 8 | return ( 9 | 10 | 11 | ReactTemplate Example 12 | 13 | 14 | {this.props.children} 15 | 16 | 17 | 18 | ); 19 | } 20 | }); 21 | 22 | 23 | module.exports = App; 24 | -------------------------------------------------------------------------------- /examples/react-template/templates/PetDetail.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | var App = require('./App'); 4 | var ReactTemplate = require('react-template'); 5 | 6 | 7 | var PetDetail = ReactTemplate.create({ 8 | render: function () { 9 | return ( 10 | 11 |
    12 |

    {this.props.petName}

    13 | See all pets! 14 |
    15 |
    16 | ); 17 | } 18 | }); 19 | 20 | module.exports = PetDetail; 21 | -------------------------------------------------------------------------------- /examples/react-template/templates/PetList.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | var App = require('./App'); 4 | var ReactTemplate = require('react-template'); 5 | 6 | var pets = [ 7 | {name: 'Winston'}, 8 | {name: 'Chaplin'}, 9 | {name: 'Bennie'} 10 | ]; 11 | 12 | var PetList = ReactTemplate.create({ 13 | renderLinks: function() { 14 | return pets.map(function(pet) { 15 | return
  • {pet.name}
  • ; 16 | }); 17 | }, 18 | render: function() { 19 | return ( 20 | 21 |
      22 | {this.renderLinks()} 23 |
    24 |
    25 | ); 26 | } 27 | }); 28 | 29 | module.exports = PetList; 30 | -------------------------------------------------------------------------------- /examples/react-template/webpack.browser.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | context: __dirname, 5 | entry: { 6 | browser: './browser.js' 7 | }, 8 | 9 | output: { 10 | filename: '[name].js', 11 | path: path.join(__dirname, 'built'), 12 | devtool: '$@inline-source-map', 13 | libraryTarget: 'umd', 14 | publicPath: '../built/' 15 | }, 16 | 17 | module: { 18 | loaders: [ 19 | {test: /\.js$/, loader: 'jsx'} 20 | ] 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /examples/react-template/webpack.server.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | context: __dirname, 5 | entry: { 6 | server: './server.js' 7 | }, 8 | externals: ['express', 'react'], 9 | target: 'node', 10 | node: { 11 | console: false, 12 | process: false, 13 | global: false, 14 | buffer: false, 15 | __filename: false, 16 | __dirname: false 17 | }, 18 | 19 | output: { 20 | filename: '[name].js', 21 | path: path.join(__dirname, 'built'), 22 | devtool: '$@inline-source-map', 23 | libraryTarget: 'commonjs2', 24 | publicPath: '../built/' 25 | }, 26 | 27 | module: { 28 | loaders: [ 29 | {test: /\.js$/, loader: 'jsx'} 30 | ] 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /examples/renderinitial/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /built 3 | -------------------------------------------------------------------------------- /examples/renderinitial/browser.js: -------------------------------------------------------------------------------- 1 | var Router = require('./router'); 2 | 3 | Router.attach(document).captureClicks(); 4 | -------------------------------------------------------------------------------- /examples/renderinitial/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorouter-renderinitial-example", 3 | "version": "1.0.0", 4 | "description": "An example of using renderInitial with monorouter", 5 | "main": "server.js", 6 | "scripts": { 7 | "build": "webpack --config webpack.browser.config.js && webpack --config webpack.server.config.js", 8 | "serve": "node ./built/server.js", 9 | "start": "npm run build && npm run serve" 10 | }, 11 | "author": "Eric Eldredge ", 12 | "license": "MIT", 13 | "dependencies": { 14 | "monorouter": "^0.7.1", 15 | "react": "^0.11.1", 16 | "monorouter-react": "^0.2.0", 17 | "connect-monorouter": "^0.2.1", 18 | "express": "^4.7.2", 19 | "webpack": "^1.3.2-beta9", 20 | "jsx-loader": "~0.11.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/renderinitial/router.js: -------------------------------------------------------------------------------- 1 | var monorouter = require('monorouter'); 2 | var reactRouting = require('monorouter-react'); 3 | var PetList = require('./views/PetList'); 4 | var PetDetail = require('./views/PetDetail'); 5 | var Preloader = require('./views/Preloader'); 6 | 7 | var pets = [ 8 | {name: 'Winston'}, 9 | {name: 'Chaplin'}, 10 | {name: 'Bennie'} 11 | ]; 12 | 13 | function fetchPetList (cb) { 14 | // Artificially make the response take a while. 15 | setTimeout(function() { 16 | cb(null, pets); 17 | }, Math.round(200 + Math.random() * 800)); 18 | } 19 | 20 | function findPetName (petName, data) { 21 | for (var i = 0; i < pets.length; i++) { 22 | var pet = data[i]; 23 | if (pet.name.toLowerCase() === petName) return pet.name; 24 | } 25 | } 26 | 27 | module.exports = monorouter() 28 | .setup(reactRouting()) 29 | .route('index', '/', function(req) { 30 | this.renderInitial(Preloader, function() { 31 | fetchPetList(function(err, data) { 32 | this.render(PetList, {pets: data}); 33 | }.bind(this)); 34 | }); 35 | }) 36 | .route('pet', '/pet/:name', function(req) { 37 | // NOTE: Because we're calling `this.renderInitial()`, we lose the 38 | // opportunity to have the server send a 404, and the client will have to 39 | // display a 'missing' view. If you want the server to send 404s, you have 40 | // to call `this.unhandled()` before `this.renderInitial()`. 41 | this.renderInitial(Preloader, function() { 42 | fetchPetList(function(err, data) { 43 | this.render(PetDetail, {petName: findPetName(req.params.name, data)}); 44 | }.bind(this)); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /examples/renderinitial/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var routerMiddleware = require('connect-monorouter'); 3 | var Router = require('./router'); 4 | var path = require('path'); 5 | 6 | port = process.env.PORT || 5000; 7 | 8 | express() 9 | .use(routerMiddleware(Router)) 10 | .get('/browser.js', function(req, res) { 11 | res.sendfile(path.join(__dirname, 'browser.js')); 12 | }) 13 | .listen(port, function() { 14 | console.log("Listening on " + port + "."); 15 | console.log("Go to in your browser."); 16 | }); 17 | -------------------------------------------------------------------------------- /examples/renderinitial/views/App.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | 4 | function App(props, children) { 5 | return ( 6 | 7 | 8 | RenderInitial Example 9 | 10 | 11 | {children} 12 | 13 | 14 | 15 | ); 16 | }; 17 | 18 | 19 | module.exports = App; 20 | -------------------------------------------------------------------------------- /examples/renderinitial/views/PetDetail.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | var App = require('./App'); 4 | 5 | function PetDetail(props) { 6 | return ( 7 | 8 |
    9 |

    {props.petName}

    10 | See all pets! 11 |
    12 |
    13 | ); 14 | } 15 | 16 | 17 | module.exports = PetDetail; 18 | -------------------------------------------------------------------------------- /examples/renderinitial/views/PetList.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | var App = require('./App'); 4 | 5 | function PetList(props) { 6 | var links = props.pets.map(function(pet) { 7 | return
  • {pet.name}
  • ; 8 | }); 9 | return ( 10 | 11 |
      12 | {links} 13 |
    14 |
    15 | ); 16 | } 17 | 18 | module.exports = PetList; 19 | -------------------------------------------------------------------------------- /examples/renderinitial/views/Preloader.js: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | var React = require('react'); 3 | var App = require('./App'); 4 | 5 | function Preloader(props) { 6 | return ( 7 | 8 |
    9 |

    Loading...

    10 |
    11 |
    12 | ); 13 | } 14 | 15 | module.exports = Preloader; 16 | -------------------------------------------------------------------------------- /examples/renderinitial/webpack.browser.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | context: __dirname, 5 | entry: { 6 | browser: './browser.js' 7 | }, 8 | 9 | output: { 10 | filename: '[name].js', 11 | path: path.join(__dirname, 'built'), 12 | devtool: '$@inline-source-map', 13 | libraryTarget: 'umd', 14 | publicPath: '../built/' 15 | }, 16 | 17 | module: { 18 | loaders: [ 19 | {test: /\.js$/, loader: 'jsx'} 20 | ] 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /examples/renderinitial/webpack.server.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = { 4 | context: __dirname, 5 | entry: { 6 | server: './server.js' 7 | }, 8 | externals: ['express', 'react'], 9 | target: 'node', 10 | node: { 11 | console: false, 12 | process: false, 13 | global: false, 14 | buffer: false, 15 | __filename: false, 16 | __dirname: false 17 | }, 18 | 19 | output: { 20 | filename: '[name].js', 21 | path: path.join(__dirname, 'built'), 22 | devtool: '$@inline-source-map', 23 | libraryTarget: 'commonjs2', 24 | publicPath: '../built/' 25 | }, 26 | 27 | module: { 28 | loaders: [ 29 | {test: /\.js$/, loader: 'jsx'} 30 | ] 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var 2 | gulp = require('gulp'), 3 | gutil = require('gulp-util'), 4 | browserify = require('gulp-browserify'), 5 | rename = require('gulp-rename'), 6 | gbump = require('gulp-bump'), 7 | jasmine = require('jasmine-runner-node'); 8 | 9 | gulp.task('watch', function() { 10 | gulp.watch('./lib/**/*', ['build:browser']); 11 | }); 12 | 13 | var bump = function(type) { 14 | gulp.src(['./bower.json', './package.json']) 15 | .pipe(gbump({type: type})) 16 | .pipe(gulp.dest('./')); 17 | }; 18 | 19 | gulp.task('bump:major', function() { bump('major'); }); 20 | gulp.task('bump:minor', function() { bump('minor'); }); 21 | gulp.task('bump:patch', function() { bump('patch'); }); 22 | 23 | gulp.task('build:browser', function() { 24 | gulp.src('./lib/index.js') 25 | .pipe(browserify({ 26 | standalone: 'monorouter', 27 | transform: ['browserify-shim'] 28 | })) 29 | .pipe(rename('monorouter.js')) 30 | .pipe(gulp.dest('./standalone/')); 31 | }); 32 | 33 | gulp.task('build:tests', function() { 34 | gulp.src('./lib/**/__tests__/*.js') 35 | .pipe(browserify({ 36 | insertGlobals : true, 37 | debug : !gulp.env.production 38 | })) 39 | .pipe(gulp.dest('./.tests-built/')); 40 | }); 41 | 42 | gulp.task('watch:tests', function() { 43 | gulp.watch('./lib/**/*', ['build:tests']); 44 | }); 45 | 46 | gulp.task('test', ['build:tests'], function() { 47 | jasmine.start({ 48 | port: 8888, 49 | files: { 50 | spec: './.tests-built/**/*.js' 51 | } 52 | }); 53 | }); 54 | 55 | gulp.task('build', ['build:browser']); 56 | -------------------------------------------------------------------------------- /lib/LinkHijacker.js: -------------------------------------------------------------------------------- 1 | var urllite = require('urllite'); 2 | require('urllite/lib/extensions/toString'); 3 | 4 | 5 | /** 6 | * A utility for hijacking link clicks and forwarding them to the router. 7 | */ 8 | function LinkHijacker(router, el, opts) { 9 | this.router = router; 10 | this.element = el || window; 11 | this.blacklist = opts && opts.blacklist; 12 | this.handleClick = this.handleClick.bind(this); 13 | this.start(); 14 | } 15 | 16 | LinkHijacker.prototype.start = function() { 17 | // This handler works by trying to route the URL and then, if it was 18 | // successful, updating the history. If the history object being used doesn't 19 | // support that, don't bother adding the event listener. 20 | if (!this.router.history.push) return; 21 | 22 | this.element.addEventListener('click', this.handleClick); 23 | }; 24 | 25 | LinkHijacker.prototype.stop = function() { 26 | this.element.removeEventListener('click', this.handleClick); 27 | }; 28 | 29 | LinkHijacker.prototype.handleClick = function(event) { 30 | // Ignore canceled events, modified clicks, and right clicks. 31 | if (event.defaultPrevented) return; 32 | if (event.metaKey || event.ctrlKey || event.shiftKey) return; 33 | if (event.button !== 0) return; 34 | 35 | // Get the element. 36 | var el = event.target; 37 | while (el && el.nodeName !== 'A') { 38 | el = el.parentNode; 39 | } 40 | 41 | // Ignore clicks from non-a elements. 42 | if (!el) return; 43 | 44 | // Ignore the click if the element has a target. 45 | if (el.target && el.target !== '_self') return; 46 | 47 | // Ignore the click if it's a download link. (We use this method of 48 | // detecting the presence of the attribute for old IE versions.) 49 | if (!!el.attributes.download) return; 50 | 51 | // Use a regular expression to parse URLs instead of relying on the browser 52 | // to do it for us (because IE). 53 | var url = urllite(el.href); 54 | var windowURL = urllite(window.location.href); 55 | 56 | // Ignore links that don't share a protocol and host with ours. 57 | if (url.protocol !== windowURL.protocol || url.host !== windowURL.host) 58 | return; 59 | 60 | var fullPath = url.pathname + url.search + url.hash; 61 | 62 | // Ignore URLs that don't share the router's rootUrl 63 | if (fullPath.indexOf(this.router.constructor.rootUrl) !== 0) return; 64 | 65 | // Ignore 'rel="external"' links. 66 | if (el.rel && /(?:^|\s+)external(?:\s+|$)/.test(el.rel)) return; 67 | 68 | // Ignore any blacklisted links. 69 | if (this.blacklist) { 70 | for (var i = 0, len = this.blacklist.length; i < len; i++) { 71 | var blacklisted = this.blacklist[i]; 72 | 73 | // The blacklist can contain strings or regexps (or anything else with a 74 | // "test" function). 75 | if (blacklisted === fullPath) return; 76 | if (blacklisted.test && blacklisted.test(fullPath)) return; 77 | } 78 | } 79 | 80 | event.preventDefault(); 81 | 82 | // Dispatch the URL. 83 | this.router.history.navigate(fullPath, {cause: 'link'}); 84 | }; 85 | 86 | module.exports = LinkHijacker; 87 | -------------------------------------------------------------------------------- /lib/Request.js: -------------------------------------------------------------------------------- 1 | var extend = require('xtend/mutable'); 2 | var queryString = require('query-string'); 3 | var urllite = require('urllite'); 4 | var Cancel = require('./errors/Cancel'); 5 | var EventEmitter = require('wolfy87-eventemitter'); 6 | var inherits = require('inherits'); 7 | require('urllite/lib/extensions/resolve'); 8 | 9 | 10 | function getUrl(url, root) { 11 | if (root && root !== '/') { 12 | var parsedRoot = urllite(root); 13 | var hostsMatch = !url.host || !parsedRoot.host || 14 | (url.host === parsedRoot.host); 15 | var inRoot = hostsMatch && url.pathname.indexOf(parsedRoot.pathname) === 0; 16 | if (inRoot) { 17 | var resolvedPath = url.pathname.slice(parsedRoot.pathname.length); 18 | resolvedPath = resolvedPath.charAt(0) === '/' ? resolvedPath : '/' + resolvedPath; 19 | return resolvedPath + url.search; 20 | } 21 | return null; 22 | } 23 | return url.pathname + url.search; 24 | } 25 | 26 | /** 27 | * An object representing the request to be routed. This object is meant to be 28 | * familiar to users of server libraries like Express and koa. 29 | */ 30 | function Request(url, opts) { 31 | var parsed = urllite(url); 32 | 33 | // Make sure we have the host information. 34 | if (!parsed.host) { 35 | if ((typeof document !== 'undefined') && document.location) { 36 | parsed = parsed.resolve(document.location.href); 37 | } else { 38 | throw new Error("You need to dispatch absolute URLs on the server."); 39 | } 40 | } 41 | 42 | extend(this, opts); 43 | this.location = parsed; 44 | this.url = getUrl(parsed, opts && opts.root); 45 | this.originalUrl = parsed.pathname + parsed.search; 46 | this.path = parsed.pathname; 47 | this.protocol = parsed.protocol.replace(/:$/, ''); 48 | this.hostname = parsed.hostname; 49 | this.host = parsed.host; 50 | this.search = parsed.search; 51 | this.querystring = parsed.search.replace(/^\?/, ''); 52 | this.query = queryString.parse(parsed.search); 53 | this.hash = parsed.hash; 54 | this.fragment = parsed.hash.replace(/^#/, ''); 55 | } 56 | 57 | // Make requests event emitters. 58 | inherits(Request, EventEmitter); 59 | 60 | /** 61 | * Get a specific param by name or index. 62 | * 63 | * @param {number|string} The index or name of the param. 64 | * @return {object} The matched variables 65 | */ 66 | Request.prototype.param = function(indexOrName) { 67 | return this.params[indexOrName]; 68 | }; 69 | 70 | /** 71 | * Check whether the request triggered by one of the the specified causes. 72 | */ 73 | Request.prototype.from = function() { 74 | var causes; 75 | if (typeof arguments[0] === 'string') causes = arguments; 76 | else causes = arguments[0]; 77 | 78 | if (causes && causes.length) { 79 | for (var i = 0, len = causes.length; i < len; i++) { 80 | if (causes[i] === this.cause) return true; 81 | } 82 | } 83 | return false; 84 | }; 85 | 86 | Request.prototype.cancel = function() { 87 | if (!this.canceled) { 88 | this.canceled = true; 89 | this.emit('cancel', new Cancel(this)); 90 | } 91 | }; 92 | 93 | Request.prototype.canceled = false; 94 | 95 | module.exports = Request; 96 | -------------------------------------------------------------------------------- /lib/Response.js: -------------------------------------------------------------------------------- 1 | var inherits = require('inherits'); 2 | var Unhandled = require('./errors/Unhandled'); 3 | var EventEmitter = require('wolfy87-eventemitter'); 4 | var extend = require('xtend'); 5 | var delayed = require('./utils/delayed'); 6 | var parallel = require('run-parallel'); 7 | var withoutResults = require('./utils/withoutResults'); 8 | var thunkifyAll = require('./utils/thunkifyAll'); 9 | var noop = require('./utils/noop'); 10 | 11 | 12 | /** 13 | * The Response is used as the `this` value for route functions. It hosts 14 | * methods for manipulating the server response and application state. 15 | */ 16 | function Response(request, router) { 17 | this.request = request; 18 | this.router = router; 19 | this.state = router.state; 20 | this.vars = {}; 21 | this._beforeRenderHooks = []; 22 | 23 | request.on('cancel', this['throw'].bind(this)); 24 | } 25 | 26 | inherits(Response, EventEmitter); 27 | 28 | Response.prototype.status = 200; 29 | 30 | // 31 | // 32 | // The main methods for manipulating the application state. 33 | // 34 | // 35 | 36 | /** 37 | * A function decorator used to prevent methods from having any effect if the 38 | * corresponding request has been canceled. This prevents you from having to 39 | * check if the request was cancled every time you do something async in your 40 | * handler (however, if you're doing a lot of work, you should do an explicit 41 | * check and opt out of it). 42 | */ 43 | function unlessCanceled(fn) { 44 | return function() { 45 | if (this.request.canceled) return this; 46 | return fn.apply(this, arguments); 47 | }; 48 | } 49 | 50 | /** 51 | * Update the view vars for any subsequent views rendered by this response. 52 | */ 53 | Response.prototype.setVars = unlessCanceled(function(vars) { 54 | this.vars = extend(this.vars, vars); 55 | return this; 56 | }); 57 | 58 | Response.prototype.setState = unlessCanceled(function(state) { 59 | this.router.setState(state); 60 | this.state = this.router.state; 61 | return this; 62 | }); 63 | 64 | Response.prototype.setView = unlessCanceled(function(view) { 65 | this.view = view; 66 | return this; 67 | }); 68 | 69 | Response.prototype.beforeRender = function(fn) { 70 | this._beforeRenderHooks.push(fn); 71 | return this; 72 | }; 73 | 74 | // 75 | // 76 | // Ending 77 | // 78 | // 79 | 80 | /** 81 | * Indicates that a response was ended. 82 | */ 83 | Response.prototype.ended = false; 84 | Response.prototype.initialEnded = false; 85 | 86 | /** 87 | * Call to indicate that router is in the "initial" state—the state that the 88 | * server will send and the browser app will begin in. 89 | */ 90 | Response.prototype.endInitial = function() { 91 | if (this.initialEnded) { 92 | return; 93 | } 94 | this.initialEnded = true; 95 | this.emit('initialReady'); 96 | if (this.request.initialOnly) { 97 | this.end(); 98 | } 99 | return this.ended; 100 | }; 101 | 102 | Response.prototype.end = function() { 103 | if (!this.ended) { 104 | this.endInitial(); 105 | this.ended = true; 106 | this.emit('end'); 107 | } 108 | }; 109 | 110 | /** 111 | * A method for declaring that the router will not handle the current request. 112 | * Under normal circumstances, you shouldn't use this method but instead simply 113 | * call your handler's `done` callback: 114 | * 115 | * Router.route('users/:username', function(req, done) { 116 | * if (this.params.username === 'Matthew') { 117 | * // Do stuff 118 | * } else { 119 | * done(); 120 | * } 121 | * }); 122 | * 123 | * This allows other middleware the opportunity to handle the request. 124 | * 125 | * Calling this method is the same as invoking the handler's callback with an 126 | * `Unhandled` error in your route handler: 127 | * 128 | * Router.route('users/:username', function() { 129 | * if (this.params.username === 'Matthew') { 130 | * // Do stuff 131 | * } else { 132 | * throw new Router.Unhandled(this.request); 133 | * } 134 | * }); 135 | * 136 | * or (the async version) 137 | * 138 | * Router.route('users/:username', function(done) { 139 | * if (this.params.username === 'Matthew') { 140 | * // Do stuff 141 | * } else { 142 | * done(new Router.Unhandled(this.request)); 143 | * } 144 | * }); 145 | */ 146 | Response.prototype.unhandled = function(msg) { 147 | this['throw'](new Unhandled(this.request, msg)); 148 | }; 149 | 150 | Response.prototype['throw'] = function(err) { 151 | if (!this.error) { 152 | this.error = err; 153 | this.emit('error', err); 154 | } 155 | }; 156 | 157 | // 158 | // 159 | // Metadata methods. 160 | // 161 | // 162 | 163 | /** 164 | * Indicate that no view was found for the corresponding request. This has no 165 | * effect in the browser, but causes a 404 response to be sent on the server. 166 | * Note that this is different from `unhandled` as it indicates that the UI has 167 | * been updated. 168 | */ 169 | Response.prototype.notFound = function() { 170 | // TODO: Should this work as a getter too? Or should we make it chainable? 171 | this.status = 404; 172 | return this; 173 | }; 174 | 175 | Response.prototype.doctype = ''; 176 | Response.prototype.contentType = 'text/html; charset=utf-8'; 177 | 178 | // 179 | // 180 | // Rendering shortcuts. 181 | // 182 | // 183 | 184 | function bindVars(view, vars) { 185 | if (vars) { 186 | var oldView = view; 187 | return function(extraVars) { 188 | return oldView.call(this, extend(vars, extraVars)); 189 | }; 190 | } 191 | return view; 192 | } 193 | 194 | /** 195 | * A function decorator that creates a render function that accepts vars (as a 196 | * second argument) from one that doesn't. 197 | * 198 | * @param {function} fn The function whose arguments to transform. 199 | */ 200 | function renderer(fn) { 201 | return function() { 202 | var view, vars, cb, callback; 203 | var args = Array.prototype.slice.call(arguments, 0); 204 | if (typeof args[0] === 'function') 205 | view = args.shift(); 206 | if (typeof args[0] === 'object') 207 | vars = args.shift(); 208 | cb = args[0]; 209 | 210 | var boundRender = function(next) { 211 | // Pass a partially applied version of the view to the original function. 212 | vars = extend(this.vars, vars); 213 | view = bindVars(view || this.view, vars); 214 | fn.call(this, view, next); 215 | }; 216 | 217 | var hooks = thunkifyAll(this._beforeRenderHooks, this); 218 | parallel(hooks, function(err) { 219 | boundRender.call(this, cb || noop); 220 | }.bind(this)); 221 | }; 222 | } 223 | 224 | // 225 | // Shortcut methods for rendering a view with props and (optionally) ending the 226 | // request. 227 | // 228 | 229 | Response.prototype._renderIntermediate = unlessCanceled(function(view, cb) { 230 | delayed(function() { 231 | this.router.setView(view); 232 | cb.call(this); 233 | }.bind(this))(); 234 | }); 235 | 236 | /** 237 | * Render the provided view and end the request. 238 | */ 239 | Response.prototype.render = renderer(function(view, cb) { 240 | this._renderIntermediate(view, function(err) { 241 | if (err) { 242 | cb(err); 243 | return; 244 | } 245 | cb.apply(this, arguments); 246 | this.end(); 247 | }); 248 | }); 249 | 250 | /** 251 | * Render the provided view and mark the current state as the initial one. 252 | */ 253 | Response.prototype.renderInitial = renderer(function(view, cb) { 254 | this._renderIntermediate(view, function(err) { 255 | if (err) { 256 | cb(err); 257 | return; 258 | } 259 | this.endInitial(); 260 | if (!this.request.initialOnly) cb.apply(this, arguments); 261 | }); 262 | }); 263 | 264 | /** 265 | * Render the provided view. 266 | */ 267 | Response.prototype.renderIntermediate = renderer(function(view, cb) { 268 | this._renderIntermediate(view, cb); 269 | }); 270 | 271 | Response.prototype.renderDocumentToString = function() { 272 | var engine = this.router.constructor.engine; 273 | var markup = engine.renderToString(this.router); 274 | return (this.doctype || '') + markup; 275 | }; 276 | 277 | module.exports = Response; 278 | -------------------------------------------------------------------------------- /lib/Route.js: -------------------------------------------------------------------------------- 1 | var pathToRegexp = require('./utils/pathToRegexp'); 2 | 3 | 4 | function Route(path) { 5 | this.path = path; 6 | this.keys = []; 7 | this.tokens = []; 8 | this.regexp = pathToRegexp(path, this.keys, this.tokens, {strict: true}); 9 | } 10 | 11 | Route.prototype.match = function(url) { 12 | var match = url.match(this.regexp); 13 | if (!match) return; 14 | var matchObj = {}; 15 | for (var i = 1, len = match.length; i < len; i++) { 16 | matchObj[i] = matchObj[this.keys[i - 1].name] = match[i]; 17 | } 18 | return matchObj; 19 | }; 20 | 21 | Route.prototype.url = function(params) { 22 | return this.tokens.map(function(token) { 23 | if (token.literal != null) return token.literal; 24 | if (token.name) { 25 | if (params && params[token.name] != null) 26 | return token.delimiter + params[token.name]; 27 | else if (token.optional) 28 | return ''; 29 | throw new Error('Missing required param "' + token.name + '"'); 30 | } 31 | }).join(''); 32 | }; 33 | 34 | module.exports = Route; 35 | -------------------------------------------------------------------------------- /lib/Router.js: -------------------------------------------------------------------------------- 1 | var extend = require('xtend'); 2 | var Route = require('./Route'); 3 | var Request = require('./Request'); 4 | var Response = require('./Response'); 5 | var Unhandled = require('./errors/Unhandled'); 6 | var delayed = require('./utils/delayed'); 7 | var getDefaultHistory = require('./history/getHistory'); 8 | var series = require('run-series'); 9 | var inherits = require('inherits'); 10 | var EventEmitter = require('wolfy87-eventemitter'); 11 | var attach = require('./attach'); 12 | var LinkHijacker = require('./LinkHijacker'); 13 | var withoutResults = require('./utils/withoutResults'); 14 | var thunkifyAll = require('./utils/thunkifyAll'); 15 | 16 | 17 | function Router(opts) { 18 | if (opts) { 19 | this.state = extend(opts.initialState); 20 | this.history = opts.history; 21 | } 22 | this.url = this.constructor.url.bind(this.constructor); 23 | // TODO: Should we add the properties from the class (i.e. rootUrl) to the instance? 24 | } 25 | 26 | inherits(Router, EventEmitter); 27 | 28 | 29 | /** 30 | * Expose the `Unhandled` error so it can be used in handlers more easily. 31 | */ 32 | Router.Unhandled = Unhandled; 33 | 34 | function throwNoEngine() { 35 | throw new Error('You are attempting to render but your router has no rendering engine.'); 36 | } 37 | 38 | Router.engine = { 39 | renderInto: throwNoEngine, 40 | renderToString: throwNoEngine 41 | }; 42 | 43 | /** 44 | * Add a route to the router class. This API can be used in a manner similar 45 | * to many server-side JS routing libraries—by chainging calls to `route`— 46 | * however, it's more likely that you'll want to pass your routes to the 47 | * router class constructor using JSX. 48 | * 49 | * This function accepts either `(path, handlers...)`, or 50 | * `(name, path, handlers...)`. 51 | */ 52 | Router.route = function() { 53 | var RouterClass = this; 54 | var args = Array.prototype.slice.call(arguments, 0); 55 | var name, path, handlers; 56 | 57 | if (typeof args[1] !== 'function') name = args.shift(); 58 | path = args.shift(); 59 | handlers = args; 60 | 61 | var route = new Route(path); 62 | 63 | if (name) { 64 | if (RouterClass.namedRoutes[name]) 65 | throw new Error('Route with name "' + name + '" already exists.'); 66 | RouterClass.namedRoutes[name] = route; 67 | } 68 | 69 | // Create and register a middleware to represent this route. 70 | RouterClass.use(function(req, next) { 71 | // If this route doesn't match, skip it. 72 | var match = route.match(req.path); 73 | if (!match) return next(); 74 | 75 | req.params = match; 76 | 77 | series(thunkifyAll(handlers, this, [req]), withoutResults(next)); 78 | }); 79 | 80 | // For chaining! 81 | return this; 82 | }; 83 | 84 | Router.prototype.setState = function(state) { 85 | this.state = extend(this.state, state); 86 | this.emit('stateChange'); 87 | }; 88 | 89 | Router.prototype.setView = function(view) { 90 | this.view = view; 91 | this.emit('viewChange'); 92 | }; 93 | 94 | Router.prototype.render = function() { 95 | if (!this.view) { 96 | throw new Error('You must set a view before rendering'); 97 | } 98 | return this.view(extend(this.state)); 99 | }; 100 | 101 | Router.finalMiddleware = [ 102 | // If we've exhausted the middleware without handling the request, call the 103 | // `unhandled()` method. Going through `unhandled` instead of just creating an 104 | // error in the dispatch callback means we're always going through the same 105 | // process, getting the same events, etc. 106 | function(req, next) { 107 | if (!this.error && !this.ended) return this.unhandled(); 108 | next(); 109 | } 110 | ]; 111 | 112 | /** 113 | * 114 | * @param {string} url 115 | * @param {function?} callback 116 | */ 117 | Router.prototype.dispatch = function(url, opts, callback) { 118 | var RouterClass = this.constructor; 119 | 120 | if (typeof arguments[1] === 'function') { 121 | callback = arguments[1]; 122 | opts = null; 123 | } 124 | 125 | // Wrap the callback, imposing a delay to force asynchronousness in 126 | // case the user calls it synchronously. 127 | var cb = function(err) { 128 | // Clean up listeners 129 | res.removeAllListeners(); 130 | req.removeAllListeners(); 131 | 132 | this._currentResponse = null; 133 | if (callback) callback(err, err ? null : res); 134 | }.bind(this); 135 | 136 | var first = (opts && opts.first) || !this._dispatchedFirstRequest; 137 | var req = new Request(url, extend(opts, { 138 | cause: opts && opts.cause, 139 | first: first, 140 | root: RouterClass.rootUrl 141 | })); 142 | this._dispatchedFirstRequest = true; 143 | 144 | var res = new Response(req, this) 145 | .on('error', cb) 146 | .on('end', cb); 147 | 148 | if (req.url == null) { 149 | delayed(function() { 150 | res.unhandled('URL not within router root: ' + url); 151 | })(); 152 | return res; 153 | } 154 | 155 | if (this._currentResponse) { 156 | this._currentResponse.request.cancel(); 157 | } 158 | this._currentResponse = res; 159 | 160 | // Force async behavior so you have a chance to add listeners to the 161 | // request object. 162 | var middleware = RouterClass.middleware.concat(RouterClass.finalMiddleware); 163 | delayed(function() { 164 | series(thunkifyAll(middleware, res, [req]), function(err) { 165 | if (err) { 166 | // Create a wrapper for each of the error middlewares that receives the 167 | // error from the previous one. This way, error middleware can pass a 168 | // different error than it received, and this new error can be handled 169 | // by subsequent error middleware. If an error middleware does not pass 170 | // an error or end the request, the request will be considered 171 | // unhandled. 172 | var previousError = err; 173 | var errorMiddleware = RouterClass.errorMiddleware.map(function(fn) { 174 | return function(done) { 175 | var newDone = function(err, result) { 176 | if (err) { 177 | previousError = err; 178 | done(); 179 | } else { 180 | res.unhandled(); 181 | } 182 | }; 183 | try { 184 | fn.call(res, previousError, req, newDone); 185 | } catch (err) { 186 | newDone(err); 187 | } 188 | }; 189 | }); 190 | series(errorMiddleware, function() { 191 | if (previousError) res['throw'](previousError); 192 | }); 193 | } 194 | }); 195 | })(); 196 | 197 | return res; 198 | }; 199 | 200 | Router.prototype.captureClicks = function(el, opts) { 201 | return new LinkHijacker(this, el, opts); 202 | }; 203 | 204 | Router.use = function(middleware) { 205 | var RouterClass = this, 206 | middlewares = middleware.length === 3 ? RouterClass.errorMiddleware : RouterClass.middleware; 207 | middlewares.push(middleware); 208 | return this; 209 | }; 210 | 211 | Router.extend = function(opts) { 212 | var SuperClass = this; 213 | var NewRouter = function(opts) { 214 | if (!(this instanceof NewRouter)) { 215 | return new NewRouter(opts); 216 | } 217 | SuperClass.call(this, opts); 218 | }; 219 | inherits(NewRouter, SuperClass); 220 | 221 | // Add "static" props to the new router. 222 | for (var k in SuperClass) { 223 | if (SuperClass.hasOwnProperty(k)) { 224 | if (k === 'super_' || k === 'prototype') continue; 225 | NewRouter[k] = SuperClass[k]; 226 | } 227 | } 228 | 229 | NewRouter.engine = opts && opts.engine; 230 | NewRouter.rootUrl = opts && opts.rootUrl || ''; 231 | NewRouter.middleware = []; 232 | NewRouter.errorMiddleware = []; 233 | NewRouter.namedRoutes = {}; 234 | 235 | return NewRouter; 236 | }; 237 | 238 | Router.attach = function(element, opts) { 239 | return attach(this, element, opts); 240 | }; 241 | 242 | /** 243 | * Extensions are just functions that mutate the router in any way they want 244 | * and return it. This function is just a prettier way to use them than calling 245 | * them directly. 246 | */ 247 | Router.setup = function(extension) { 248 | var router = extension(this); 249 | if (!router) 250 | throw new Error('Invalid extension: extension did not return router.'); 251 | return router; 252 | }; 253 | 254 | Router.url = function(name, params) { 255 | var route = this.namedRoutes[name]; 256 | if (!route) 257 | throw new Error('There is no route named "' + name + '".'); 258 | return route.url(params); 259 | }; 260 | 261 | module.exports = Router; 262 | -------------------------------------------------------------------------------- /lib/__tests__/Request-test.js: -------------------------------------------------------------------------------- 1 | var Request = require('../Request'); 2 | 3 | 4 | describe('Request', function() { 5 | 6 | it('has no hash in the `url` or `originalUrl` properties', function() { 7 | var req = new Request('http://example.com/a/b/c?search=hi#yup'); 8 | expect(req.url).not.toContain('#'); 9 | expect(req.originalUrl).not.toContain('#'); 10 | }); 11 | 12 | it('exposes the hash', function() { 13 | var req = new Request('http://example.com/a/b/c?search=hi#yup'); 14 | expect(req.fragment).toBe('yup'); 15 | }); 16 | 17 | it('contains the domain', function() { 18 | var req = new Request('http://example.com/a/b/c'); 19 | expect(req.hostname).toBe('example.com'); 20 | }); 21 | 22 | it('has the path as its url property', function() { 23 | var req = new Request('http://example.com/a/b/c?search=hi#yup'); 24 | expect(req.url).toBe('/a/b/c?search=hi'); 25 | }); 26 | 27 | it('resolves the url to a root', function() { 28 | var req = new Request('http://example.com/a/b/c?search=hi#yup', 29 | {root: '/a/b'}); 30 | expect(req.url).toBe('/c?search=hi'); 31 | }); 32 | 33 | it('resolves the url to a root with a trailing slash', function() { 34 | var req = new Request('http://example.com/a/b/c?search=hi#yup', 35 | {root: '/a/b/'}); 36 | expect(req.url).toBe('/c?search=hi'); 37 | }); 38 | 39 | it('contains the original, unresolved url', function() { 40 | var req = new Request('http://example.com/a/b/c?search=hi#yup', 41 | {root: '/a/b/'}); 42 | expect(req.originalUrl).toBe('/a/b/c?search=hi'); 43 | }); 44 | 45 | it('has no `url` property when the URL is outside the root', function() { 46 | var req = new Request('http://example.com/a/b/c?search=hi', 47 | {root: '/x/y'}); 48 | expect(req.url).toBe(null); 49 | expect(req.originalUrl).toBe('/a/b/c?search=hi'); 50 | }); 51 | 52 | it('can tell you the cause', function() { 53 | var req = new Request('http://example.com/a/b/c?search=hi', 54 | {cause: 'popstate'}); 55 | expect(req.from('popstate')).toBe(true); 56 | expect(req.from('somethingelse')).toBe(false); 57 | expect(req.from('somethingelse', 'popstate')).toBe(true); 58 | expect(req.from(['somethingelse', 'popstate'])).toBe(true); 59 | expect(req.from(['somethingelse'])).toBe(false); 60 | }); 61 | 62 | it('should expose all the opts', function() { 63 | var req = new Request('', 64 | { 65 | first: false, 66 | initialOnly: true, 67 | originalReq: {foo: true} 68 | }); 69 | expect(req.first).toBe(false); 70 | expect(req.initialOnly).toBe(true); 71 | expect(req.originalReq.foo).toBe(true); 72 | }); 73 | 74 | }); 75 | -------------------------------------------------------------------------------- /lib/__tests__/Route-test.js: -------------------------------------------------------------------------------- 1 | var Route = require('../Route'); 2 | 3 | 4 | describe('Route', function() { 5 | describe('#url()', function() { 6 | 7 | it('generates a URL', function() { 8 | var route = new Route('/users/'); 9 | var url = route.url(); 10 | expect(url).toBe('/users/'); 11 | }); 12 | 13 | it('generates a URL with a named param', function() { 14 | var route = new Route('/users/:username/pets'); 15 | var url = route.url({username: 'matthewwithanm'}); 16 | expect(url).toBe('/users/matthewwithanm/pets'); 17 | }); 18 | 19 | it("errors if you don't provide all the params", function() { 20 | var route = new Route('/users/:username/pets'); 21 | expect(route.url.bind(route)).toThrowError(/Missing required param/); 22 | }); 23 | 24 | it("properly builds URLs with special RegExp chars", function() { 25 | var route = new Route('/weird,^url/:username/pets'); 26 | var url = route.url({username: 'matthewwithanm'}); 27 | expect(url).toBe('/weird,^url/matthewwithanm/pets'); 28 | }); 29 | 30 | it("properly builds URLs with optional params", function() { 31 | var route = new Route('/users/:username?'); 32 | var url = route.url({username: 'matthewwithanm'}); 33 | expect(url).toBe('/users/matthewwithanm'); 34 | expect(route.url()).toBe('/users'); 35 | }); 36 | 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /lib/__tests__/Router-test.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var monorouter = require('../index'); 3 | var DummyHistory = require('../history/DummyHistory'); 4 | var Route = require('../Route'); 5 | var reactRouting = require('monorouter-react'); 6 | 7 | 8 | var noop = function() {}; 9 | var dummy = function(props) { 10 | return React.DOM.div(null, props.message); 11 | }; 12 | 13 | function renderComponent(component, block) { 14 | var div = document.createElement('div'); 15 | React.renderComponent(component, div); 16 | if (block) 17 | block(); 18 | } 19 | 20 | describe('A Router instance', function() { 21 | 22 | it('creates routes', function() { 23 | var Router = monorouter() 24 | .route('a', '/a', noop) 25 | .route('b', '/b', noop); 26 | expect(Router.middleware.length).toBe(2); 27 | }); 28 | 29 | it('matches routes', function(done) { 30 | var Router = monorouter() 31 | .route('/animals/:type', function(req) { 32 | expect(req.params.type).toBe('dog'); 33 | done(); 34 | }); 35 | Router().dispatch('/animals/dog', function(err) { 36 | if (err) done(err); 37 | }); 38 | }); 39 | 40 | it('throws Unhandled errors for…um, unhandled URLs', function(done) { 41 | var Router = monorouter(); 42 | Router() 43 | .dispatch('/animals/dog', function(err) { 44 | expect(err).toBeTruthy(); 45 | expect(err.name).toBe('Unhandled'); 46 | done(); 47 | }); 48 | }); 49 | 50 | it('throws Unhandled errors when we explicitly choose not to handle', function(done) { 51 | var Router = monorouter() 52 | .route('/animals/:type', function(req) { 53 | this.unhandled(); 54 | }); 55 | Router().dispatch('/animals/dog', function(err) { 56 | expect(err).toBeTruthy(); 57 | expect(err.name).toBe('Unhandled'); 58 | done(); 59 | }); 60 | }); 61 | 62 | it('cancels pending requests', function(done) { 63 | var div = document.createElement('div'); 64 | var history = new DummyHistory('/async'); 65 | var Router = monorouter() 66 | .setup(reactRouting()) 67 | .route('/async', function(req) { 68 | req.on('cancel', function(err) { 69 | done(); 70 | }); 71 | setTimeout(function() { 72 | this.render(dummy); 73 | }.bind(this), 0); 74 | }) 75 | .route('/cancelit', function(req) { 76 | this.render(dummy); 77 | }); 78 | Router.attach(div, {history: history}); 79 | history.push('/cancelit'); 80 | }); 81 | 82 | it('Updates the router state', function(done){ 83 | var Router = monorouter() 84 | .route('/test', function() { 85 | this.success = true; 86 | this.render(dummy); 87 | }); 88 | var router = Router(); 89 | router.dispatch('/test', function(err, res) { 90 | if (err) return done(err); 91 | expect(res.success).toBeTruthy(); 92 | done(); 93 | }); 94 | }); 95 | 96 | it('invokes the beforeRender hooks in turn', function(done){ 97 | var order = []; 98 | var Router = monorouter() 99 | .use(function(req, next) { 100 | this 101 | .beforeRender(function(cb) { 102 | setTimeout(function() { 103 | order.push(2); 104 | cb(); 105 | }, 0); 106 | }) 107 | .beforeRender(function(cb) { 108 | setTimeout(function() { 109 | order.push(3); 110 | cb(); 111 | }, 0); 112 | }); 113 | next(); 114 | }) 115 | .route('/test', function() { 116 | order.push(1); 117 | this.render(dummy); 118 | }); 119 | var router = Router(); 120 | router.dispatch('/test', function(err, res) { 121 | if (err) return done(err); 122 | expect(order).toEqual([1, 2, 3]); 123 | done(); 124 | }); 125 | }); 126 | 127 | it('allows the view to be set without rendering', function(done) { 128 | var view = function(vars) { 129 | expect(vars.value).toBe(5); 130 | done(); 131 | return React.DOM.div(); 132 | }; 133 | var Router = monorouter() 134 | .use(function(req, next) { 135 | this.setView(view); 136 | next(); 137 | }) 138 | .route('/test', function() { 139 | this.render({value: 5}); 140 | }); 141 | var router = Router(); 142 | router.dispatch('/test', function(err, res) { 143 | router.render(); 144 | }); 145 | }); 146 | 147 | it('allows view and vars to be built up prior to render', function(done) { 148 | var view = function(vars) { 149 | expect(vars.value).toBe(5); 150 | done(); 151 | return React.DOM.div(); 152 | }; 153 | var Router = monorouter() 154 | .use(function(req, next) { 155 | this.setView(view); 156 | next(); 157 | }) 158 | .use(function(req, next) { 159 | this.setVars({value: 5}); 160 | next(); 161 | }) 162 | .route('/test', function() { 163 | this.render(); 164 | }); 165 | var router = Router(); 166 | router.dispatch('/test', function(err, res) { 167 | router.render(); 168 | }); 169 | }); 170 | 171 | describe('Error middleware', function() { 172 | it('is called if middleware passes an error', function(done) { 173 | var ERR_NAME = 'Test error'; 174 | var Router = monorouter() 175 | .route('/animals/:type', function(req, next) { 176 | var err = new Error(); 177 | err.name = ERR_NAME; 178 | next(err); 179 | }) 180 | .use(function(req, next) { 181 | throw new Error('This should not be called!'); 182 | }) 183 | .use(function(err, req, next) { 184 | expect(err).toBeTruthy(); 185 | expect(err.name).toBe(ERR_NAME); 186 | done(); 187 | }); 188 | Router().dispatch('/animals/dog', function(err) { 189 | if (err) done(err); 190 | }); 191 | }); 192 | 193 | it('is called if middleware throws an error', function(done) { 194 | var ERR_NAME = 'Test error'; 195 | var Router = monorouter() 196 | .route('/animals/:type', function(req, next) { 197 | var err = new Error(); 198 | err.name = ERR_NAME; 199 | throw err; 200 | }) 201 | .use(function(req, next) { 202 | throw new Error('This should not be called!'); 203 | }) 204 | .use(function(err, req, next) { 205 | expect(err).toBeTruthy(); 206 | expect(err.name).toBe(ERR_NAME); 207 | done(); 208 | }); 209 | Router().dispatch('/animals/dog', function(err) { 210 | if (err) done(err); 211 | }); 212 | }); 213 | 214 | it('treats not passing the error as an unhandled request', function(done) { 215 | var Router = monorouter() 216 | .route('/animals/:type', function(req, next) { 217 | throw new Error(); 218 | }) 219 | .use(function(err, req, next) { 220 | // Don't pass along the error. 221 | next(); 222 | }); 223 | Router().dispatch('/animals/dog', function(err) { 224 | expect(err && err.name).toBe('Unhandled'); 225 | done(); 226 | }); 227 | }); 228 | 229 | it('passes errors down the chain', function(done) { 230 | var FIRST_ERROR_NAME = 'First'; 231 | var SECOND_ERROR_NAME = 'Second'; 232 | var Router = monorouter() 233 | .route('/animals/:type', function(req, next) { 234 | var err = new Error(); 235 | err.name = FIRST_ERROR_NAME; 236 | throw err; 237 | }) 238 | .use(function(err, req, next) { 239 | // Pass along another error. 240 | expect(err && err.name).toBe(FIRST_ERROR_NAME); 241 | var err = new Error(); 242 | err.name = SECOND_ERROR_NAME; 243 | next(err); 244 | }) 245 | .use(function(err, req, next) { 246 | // Pass along another error. 247 | expect(err && err.name).toBe(SECOND_ERROR_NAME); 248 | done(); 249 | }); 250 | Router().dispatch('/animals/dog'); 251 | }); 252 | 253 | it('is passes the error to the dispatch callback', function(done) { 254 | var ERR_NAME = 'Test error'; 255 | var Router = monorouter() 256 | .route('/animals/:type', function(req, next) { 257 | throw new Error(); 258 | }) 259 | .use(function(err, req, next) { 260 | var err = new Error(); 261 | err.name = ERR_NAME; 262 | next(err); 263 | }); 264 | Router().dispatch('/animals/dog', function(err) { 265 | expect(err && err.name).toBe(ERR_NAME); 266 | done(); 267 | }); 268 | }); 269 | }); 270 | 271 | }); 272 | -------------------------------------------------------------------------------- /lib/attach.js: -------------------------------------------------------------------------------- 1 | var getDefaultHistory = require('./history/getHistory'); 2 | 3 | 4 | /** 5 | * Bootstraps the app by getting the initial state. 6 | */ 7 | function attach(Router, element, opts) { 8 | if (!opts) opts = {}; 9 | var history = opts.history || getDefaultHistory(); 10 | var router = new Router({history: history}); 11 | 12 | var render = function() { 13 | Router.engine.renderInto(router, element); 14 | }; 15 | 16 | var onInitialReady = function() { 17 | render(); 18 | router 19 | .on('viewChange', render) 20 | .on('stateChange', render); 21 | 22 | // Now that the view has been bootstrapped (i.e. is in its inital state), it 23 | // can be updated. 24 | update(); 25 | history.on('update', function(meta) { 26 | update(meta); 27 | }); 28 | }; 29 | 30 | var previousURL; 31 | var update = function(meta) { 32 | var url = history.currentURL(); 33 | if (url === previousURL) return; 34 | previousURL = url; 35 | 36 | var res = router.dispatch(url, meta, function(err) { 37 | if (err && (err.name !== 'Unhandled') && (err.name !== 'Cancel')) { 38 | throw err; 39 | } 40 | }); 41 | 42 | if (meta && meta.cause === 'startup') { 43 | res.once('initialReady', onInitialReady); 44 | } 45 | }; 46 | 47 | // Start the process. 48 | update({cause: 'startup'}); 49 | 50 | return router; 51 | } 52 | 53 | module.exports = attach; 54 | -------------------------------------------------------------------------------- /lib/errors/Cancel.js: -------------------------------------------------------------------------------- 1 | var initError = require('./initError'); 2 | 3 | function Cancel(request) { 4 | initError(this, 'Cancel', 'Request was canceled: ' + request.path); 5 | this.request = request; 6 | } 7 | 8 | Cancel.prototype = Error.prototype; 9 | 10 | module.exports = Cancel; 11 | -------------------------------------------------------------------------------- /lib/errors/Unhandled.js: -------------------------------------------------------------------------------- 1 | var initError = require('./initError'); 2 | 3 | function Unhandled(request, msg) { 4 | if (!msg) msg = 'Path not found: ' + request.path; 5 | initError(this, 'Unhandled', msg); 6 | this.request = request; 7 | } 8 | 9 | Unhandled.prototype = Error.prototype; 10 | 11 | module.exports = Unhandled; 12 | -------------------------------------------------------------------------------- /lib/errors/initError.js: -------------------------------------------------------------------------------- 1 | function initError(error, name, msg) { 2 | var source = new Error(msg); 3 | error.name = source.name = name; 4 | error.message = source.message; 5 | 6 | if (source.stack) { 7 | error.stack = source.stack; 8 | } 9 | 10 | error.toString = function() { 11 | return this.name + ': ' + this.message; 12 | } 13 | }; 14 | 15 | module.exports = initError; 16 | -------------------------------------------------------------------------------- /lib/history/BaseHistory.js: -------------------------------------------------------------------------------- 1 | var inherits = require('inherits'); 2 | var EventEmitter = require('wolfy87-eventemitter'); 3 | 4 | 5 | function BaseHistory() {} 6 | 7 | // Make BaseHistory an event emitter. 8 | inherits(BaseHistory, EventEmitter); 9 | 10 | /** 11 | * Navigate to the provided URL without creating a duplicate history entry if 12 | * you're already there. 13 | */ 14 | BaseHistory.prototype.navigate = function(url, meta) { 15 | if (url !== this.currentURL()) { 16 | this.push(url, meta); 17 | } else { 18 | this.replace(url, meta); 19 | } 20 | }; 21 | 22 | module.exports = BaseHistory; 23 | -------------------------------------------------------------------------------- /lib/history/DummyHistory.js: -------------------------------------------------------------------------------- 1 | var inherits = require('inherits'); 2 | var BaseHistory = require('./BaseHistory'); 3 | 4 | 5 | var DummyHistory = function(initialPath) { 6 | this.history = [initialPath]; 7 | }; 8 | 9 | inherits(DummyHistory, BaseHistory); 10 | 11 | DummyHistory.prototype.currentURL = function() { 12 | return this.history[this.history.length - 1]; 13 | }; 14 | 15 | DummyHistory.prototype.push = function(path, meta) { 16 | this.history.push(path); 17 | this.emit('update', meta); 18 | }; 19 | 20 | DummyHistory.prototype.replace = function(path, meta) { 21 | this.history.pop(); 22 | this.history.push(path); 23 | this.emit('update', meta); 24 | }; 25 | 26 | DummyHistory.prototype.pop = function() { 27 | throw new Error('Not Implemented'); 28 | }; 29 | 30 | module.exports = DummyHistory; 31 | -------------------------------------------------------------------------------- /lib/history/FallbackHistory.js: -------------------------------------------------------------------------------- 1 | var inherits = require('inherits'); 2 | var BaseHistory = require('./BaseHistory'); 3 | var urllite = require ('urllite'); 4 | 5 | 6 | /** 7 | * A history interface for browsers that don't support pushState. 8 | */ 9 | function FallbackHistory() {} 10 | 11 | inherits(FallbackHistory, BaseHistory); 12 | 13 | FallbackHistory.prototype.currentURL = function() { 14 | // If we have our own idea of the URL, use that. 15 | if (this._url) return this._url; 16 | 17 | // Use urllite to pave over IE issues with pathname. 18 | var parsed = urllite(document.location.href); 19 | return parsed.pathname + parsed.search + parsed.hash; 20 | }; 21 | 22 | FallbackHistory.prototype.push = function(path) { 23 | // No need to update `this._url`—this code is all going to be reloaded. 24 | window.location = path; 25 | }; 26 | 27 | FallbackHistory.prototype.replace = function(path, meta) { 28 | // For the fallback history, `replace` won't actually change the browser 29 | // address, but will update its own URL. This is because `replace` usually 30 | // corresponds to "lesser" state changes: having a stale browser URL is 31 | // considered more acceptable than refreshing the entire page. 32 | this._url = path; 33 | this.emit('update', meta); 34 | }; 35 | 36 | module.exports = FallbackHistory; 37 | -------------------------------------------------------------------------------- /lib/history/History.js: -------------------------------------------------------------------------------- 1 | var PushStateHistory = require('./PushStateHistory'); 2 | var FallbackHistory = require('./FallbackHistory'); 3 | 4 | 5 | var win = typeof window !== 'undefined' ? window : null; 6 | var history = win && win.history; 7 | 8 | module.exports = history && history.pushState ? PushStateHistory : FallbackHistory; 9 | -------------------------------------------------------------------------------- /lib/history/PushStateHistory.js: -------------------------------------------------------------------------------- 1 | var inherits = require('inherits'); 2 | var BaseHistory = require('./BaseHistory'); 3 | var urllite = require ('urllite'); 4 | 5 | 6 | /** 7 | * A history implementation that uses `pushState`. 8 | */ 9 | function PushStateHistory() { 10 | window.addEventListener('popstate', function(event) { 11 | this.emit('update', {cause: 'popstate'}); 12 | }.bind(this)); 13 | } 14 | 15 | inherits(PushStateHistory, BaseHistory); 16 | 17 | PushStateHistory.prototype.currentURL = function() { 18 | // Use urllite to pave over IE issues with pathname. 19 | var parsed = urllite(document.location.href); 20 | return parsed.pathname + parsed.search + parsed.hash; 21 | }; 22 | 23 | PushStateHistory.prototype.push = function(path, meta) { 24 | window.history.pushState({}, '', path); 25 | this.emit('update', meta); 26 | }; 27 | 28 | PushStateHistory.prototype.replace = function(path, meta) { 29 | window.history.replaceState({}, '', path); 30 | this.emit('update', meta); 31 | }; 32 | 33 | module.exports = PushStateHistory; 34 | -------------------------------------------------------------------------------- /lib/history/getHistory.js: -------------------------------------------------------------------------------- 1 | var History = require('./History'); 2 | 3 | var singleton; 4 | 5 | function getHistory() { 6 | if (!singleton) 7 | singleton = new History(); 8 | return singleton; 9 | } 10 | 11 | module.exports = getHistory; 12 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var Router = require('./Router'); 2 | 3 | function monorouter(opts) { 4 | return Router.extend(opts); 5 | } 6 | 7 | module.exports = monorouter; 8 | -------------------------------------------------------------------------------- /lib/utils/delayed.js: -------------------------------------------------------------------------------- 1 | var delay = typeof setImmediate === 'function' ? setImmediate : function(fn) { 2 | setTimeout(fn, 0); 3 | }; 4 | 5 | /** 6 | * Creates a delayed version of the provided function. This is used to guarantee 7 | * ansynchronous behavior for potentially synchronous operations. 8 | */ 9 | function delayed(fn) { 10 | return function() { 11 | var args = arguments; 12 | var self = this; 13 | var fnWithArgs = function() { 14 | fn.apply(self, args); 15 | }; 16 | delay(fnWithArgs); 17 | }; 18 | } 19 | 20 | module.exports = delayed; 21 | -------------------------------------------------------------------------------- /lib/utils/noop.js: -------------------------------------------------------------------------------- 1 | module.exports = function() {}; 2 | -------------------------------------------------------------------------------- /lib/utils/pathToRegexp.js: -------------------------------------------------------------------------------- 1 | // A custom version of Blake Embrey's path-to-regexp—modified in order to 2 | // support URL reversal. 3 | 4 | /* 5 | * The MIT License (MIT) 6 | * 7 | * Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) 8 | * 9 | * Permission is hereby granted, free of charge, to any person obtaining a copy 10 | * of this software and associated documentation files (the "Software"), to deal 11 | * in the Software without restriction, including without limitation the rights 12 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | * copies of the Software, and to permit persons to whom the Software is 14 | * furnished to do so, subject to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be included in 17 | * all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | * THE SOFTWARE. 26 | */ 27 | 28 | /** 29 | * Expose `pathtoRegexp`. 30 | */ 31 | module.exports = pathtoRegexp; 32 | 33 | var PATH_REGEXP = new RegExp([ 34 | // Match already escaped characters that would otherwise incorrectly appear 35 | // in future matches. This allows the user to escape special characters that 36 | // shouldn't be transformed. 37 | '(\\\\.)', 38 | // Match Express-style parameters and un-named parameters with a prefix 39 | // and optional suffixes. Matches appear as: 40 | // 41 | // "/:test(\\d+)?" => ["/", "test", "\d+", undefined, "?"] 42 | // "/route(\\d+)" => [undefined, undefined, undefined, "\d+", undefined] 43 | '([\\/.])?(?:\\:(\\w+)(?:\\(((?:\\\\.|[^)])*)\\))?|\\(((?:\\\\.|[^)])*)\\))([+*?])?', 44 | // Match regexp special characters that should always be escaped. 45 | '([.+*?=^!:${}()[\\]|\\/])' 46 | ].join('|'), 'g'); 47 | 48 | /** 49 | * Escape the capturing group by escaping special characters and meaning. 50 | * 51 | * @param {String} group 52 | * @return {String} 53 | */ 54 | function escapeGroup (group) { 55 | return group.replace(/([=!:$\/()])/g, '\\$1'); 56 | } 57 | 58 | /** 59 | * Normalize the given path string, returning a regular expression. 60 | * 61 | * An empty array should be passed in, which will contain the placeholder key 62 | * names. For example `/user/:id` will then contain `["id"]`. 63 | * 64 | * @param {(String|RegExp|Array)} path 65 | * @param {Array} keys 66 | * @param {Array} tokens 67 | * @param {Object} options 68 | * @return {RegExp} 69 | */ 70 | function pathtoRegexp (path, keys, tokens, options) { 71 | keys = keys || []; 72 | tokens = tokens || []; 73 | options = options || {}; 74 | 75 | var strict = options.strict; 76 | var end = options.end !== false; 77 | var flags = options.sensitive ? '' : 'i'; 78 | var index = 0; 79 | 80 | var charIndex = 0; 81 | var originalPath = path; 82 | 83 | // Alter the path string into a usable regexp. 84 | path = path.replace(PATH_REGEXP, function (match, escaped, prefix, key, capture, group, suffix, escape, offset) { 85 | 86 | if (offset !== charIndex) { 87 | tokens.push({literal: path.slice(charIndex, offset)}); 88 | } 89 | charIndex = offset + match.length; 90 | 91 | // Avoiding re-escaping escaped characters. 92 | if (escaped) { 93 | return escaped; 94 | } 95 | 96 | // Escape regexp special characters. 97 | if (escape) { 98 | tokens.push({literal: escape}); 99 | return '\\' + escape; 100 | } 101 | 102 | var repeat = suffix === '+' || suffix === '*'; 103 | var optional = suffix === '?' || suffix === '*'; 104 | 105 | keys.push({ 106 | name: key || index++, 107 | delimiter: prefix || '/', 108 | optional: optional, 109 | repeat: repeat 110 | }); 111 | tokens.push(keys[keys.length - 1]); 112 | 113 | // Escape the prefix character. 114 | prefix = prefix ? '\\' + prefix : ''; 115 | 116 | // Match using the custom capturing group, or fallback to capturing 117 | // everything up to the next slash (or next period if the param was 118 | // prefixed with a period). 119 | capture = escapeGroup(capture || group || '[^' + (prefix || '\\/') + ']+?'); 120 | 121 | // Allow parameters to be repeated more than once. 122 | if (repeat) { 123 | capture = capture + '(?:' + prefix + capture + ')*'; 124 | } 125 | 126 | // Allow a parameter to be optional. 127 | if (optional) { 128 | return '(?:' + prefix + '(' + capture + '))?'; 129 | } 130 | 131 | // Basic parameter support. 132 | return prefix + '(' + capture + ')'; 133 | }); 134 | 135 | // Add any unconsumed part of the string to our token list. 136 | if (charIndex !== originalPath.length) { 137 | tokens.push({literal: originalPath.slice(charIndex, originalPath.length)}); 138 | } 139 | 140 | // Check whether the path ends in a slash as it alters some match behaviour. 141 | var endsWithSlash = path[path.length - 1] === '/'; 142 | 143 | // In non-strict mode we allow an optional trailing slash in the match. If 144 | // the path to match already ended with a slash, we need to remove it for 145 | // consistency. The slash is only valid at the very end of a path match, not 146 | // anywhere in the middle. This is important for non-ending mode, otherwise 147 | // "/test/" will match "/test//route". 148 | if (!strict) { 149 | path = (endsWithSlash ? path.slice(0, -2) : path) + '(?:\\/(?=$))?'; 150 | } 151 | 152 | // In non-ending mode, we need prompt the capturing groups to match as much 153 | // as possible by using a positive lookahead for the end or next path segment. 154 | if (!end) { 155 | path += strict && endsWithSlash ? '' : '(?=\\/|$)'; 156 | } 157 | 158 | return new RegExp('^' + path + (end ? '$' : ''), flags); 159 | } 160 | -------------------------------------------------------------------------------- /lib/utils/thunkifyAll.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Return thunkified versions of the provided functions bound to the specified 3 | * context and with the provided initial args. 4 | */ 5 | function thunkifyAll(fns, thisArg, args) { 6 | fns = fns || []; 7 | return fns.map(function(fn) { 8 | return function(done) { 9 | var newArgs = args ? Array.prototype.slice.call(args, 0) : []; 10 | newArgs.push(done); 11 | try { 12 | fn.apply(thisArg, newArgs); 13 | } catch (err) { 14 | done(err); 15 | } 16 | }; 17 | }); 18 | } 19 | 20 | module.exports = thunkifyAll; 21 | -------------------------------------------------------------------------------- /lib/utils/withoutResults.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a new version of a callback that doesn't get the results. This is 3 | * used so that we don't forward collected results from run-series. 4 | */ 5 | function withoutResults(callback, thisArg) { 6 | if (callback) { 7 | return function(err, results) { 8 | return callback.call(thisArg, err); 9 | }; 10 | } 11 | } 12 | 13 | module.exports = withoutResults; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "monorouter", 3 | "version": "0.11.3", 4 | "description": "An isomorphic JS router for the virtual DOM", 5 | "main": "./lib/index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "dependencies": { 10 | "urllite": "~0.4.2", 11 | "query-string": "~0.4.1", 12 | "wolfy87-eventemitter": "~4.2.6", 13 | "inherits": "~2.0.1", 14 | "xtend": "~3.0.0", 15 | "run-series": "~1.0.2", 16 | "run-parallel": "~1.0.0" 17 | }, 18 | "devDependencies": { 19 | "browserify": "~3.33.0", 20 | "browserify-shim": "~3.3.2", 21 | "gulp": "~3.5.6", 22 | "gulp-bump": "~0.1.6", 23 | "gulp-util": "~2.2.14", 24 | "gulp-browserify": "~0.5.0", 25 | "gulp-rename": "~1.2.0", 26 | "webpack": "^1.3.1-beta4", 27 | "jsx-loader": "^0.10.2", 28 | "imports-loader": "^0.6.3", 29 | "express": "^4.5.1", 30 | "jasmine-runner-node": "0.0.1", 31 | "react": "*", 32 | "monorouter-react": "^0.3.0" 33 | }, 34 | "scripts": { 35 | "prepublish": "gulp build", 36 | "test": "gulp test" 37 | }, 38 | "repository": { 39 | "type": "git", 40 | "url": "git://github.com/matthewwithanm/monorouter.git" 41 | }, 42 | "keywords": [ 43 | "react-component", 44 | "react", 45 | "application", 46 | "router", 47 | "routing", 48 | "component" 49 | ], 50 | "author": "Matthew Dapena-Tretter ", 51 | "license": "MIT", 52 | "bugs": { 53 | "url": "https://github.com/matthewwithanm/monorouter/issues" 54 | }, 55 | "browserify-shim": { 56 | "react": "global:React" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /standalone/monorouter.js: -------------------------------------------------------------------------------- 1 | !function(e){if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else{var o;"undefined"!=typeof window?o=window:"undefined"!=typeof global?o=global:"undefined"!=typeof self&&(o=self),o.monorouter=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o element. 37 | var el = event.target; 38 | while (el && el.nodeName !== 'A') { 39 | el = el.parentNode; 40 | } 41 | 42 | // Ignore clicks from non-a elements. 43 | if (!el) return; 44 | 45 | // Ignore the click if the element has a target. 46 | if (el.target && el.target !== '_self') return; 47 | 48 | // Ignore the click if it's a download link. (We use this method of 49 | // detecting the presence of the attribute for old IE versions.) 50 | if (!!el.attributes.download) return; 51 | 52 | // Use a regular expression to parse URLs instead of relying on the browser 53 | // to do it for us (because IE). 54 | var url = urllite(el.href); 55 | var windowURL = urllite(window.location.href); 56 | 57 | // Ignore links that don't share a protocol and host with ours. 58 | if (url.protocol !== windowURL.protocol || url.host !== windowURL.host) 59 | return; 60 | 61 | var fullPath = url.pathname + url.search + url.hash; 62 | 63 | // Ignore URLs that don't share the router's rootUrl 64 | if (fullPath.indexOf(this.router.constructor.rootUrl) !== 0) return; 65 | 66 | // Ignore 'rel="external"' links. 67 | if (el.rel && /(?:^|\s+)external(?:\s+|$)/.test(el.rel)) return; 68 | 69 | // Ignore any blacklisted links. 70 | if (this.blacklist) { 71 | for (var i = 0, len = this.blacklist.length; i < len; i++) { 72 | var blacklisted = this.blacklist[i]; 73 | 74 | // The blacklist can contain strings or regexps (or anything else with a 75 | // "test" function). 76 | if (blacklisted === fullPath) return; 77 | if (blacklisted.test && blacklisted.test(fullPath)) return; 78 | } 79 | } 80 | 81 | event.preventDefault(); 82 | 83 | // Dispatch the URL. 84 | this.router.history.navigate(fullPath, {cause: 'link'}); 85 | }; 86 | 87 | module.exports = LinkHijacker; 88 | 89 | },{"urllite":25,"urllite/lib/extensions/toString":30}],2:[function(_dereq_,module,exports){ 90 | var extend = _dereq_('xtend/mutable'); 91 | var queryString = _dereq_('query-string'); 92 | var urllite = _dereq_('urllite'); 93 | var Cancel = _dereq_('./errors/Cancel'); 94 | var EventEmitter = _dereq_('wolfy87-eventemitter'); 95 | var inherits = _dereq_('inherits'); 96 | _dereq_('urllite/lib/extensions/resolve'); 97 | 98 | 99 | function getUrl(url, root) { 100 | if (root && root !== '/') { 101 | var parsedRoot = urllite(root); 102 | var hostsMatch = !url.host || !parsedRoot.host || 103 | (url.host === parsedRoot.host); 104 | var inRoot = hostsMatch && url.pathname.indexOf(parsedRoot.pathname) === 0; 105 | if (inRoot) { 106 | var resolvedPath = url.pathname.slice(parsedRoot.pathname.length); 107 | resolvedPath = resolvedPath.charAt(0) === '/' ? resolvedPath : '/' + resolvedPath; 108 | return resolvedPath + url.search; 109 | } 110 | return null; 111 | } 112 | return url.pathname + url.search; 113 | } 114 | 115 | /** 116 | * An object representing the request to be routed. This object is meant to be 117 | * familiar to users of server libraries like Express and koa. 118 | */ 119 | function Request(url, opts) { 120 | var parsed = urllite(url); 121 | 122 | // Make sure we have the host information. 123 | if (!parsed.host) { 124 | if ((typeof document !== 'undefined') && document.location) { 125 | parsed = parsed.resolve(document.location.href); 126 | } else { 127 | throw new Error("You need to dispatch absolute URLs on the server."); 128 | } 129 | } 130 | 131 | extend(this, opts); 132 | this.location = parsed; 133 | this.url = getUrl(parsed, opts && opts.root); 134 | this.originalUrl = parsed.pathname + parsed.search; 135 | this.path = parsed.pathname; 136 | this.protocol = parsed.protocol.replace(/:$/, ''); 137 | this.hostname = parsed.hostname; 138 | this.host = parsed.host; 139 | this.search = parsed.search; 140 | this.querystring = parsed.search.replace(/^\?/, ''); 141 | this.query = queryString.parse(parsed.search); 142 | this.hash = parsed.hash; 143 | this.fragment = parsed.hash.replace(/^#/, ''); 144 | } 145 | 146 | // Make requests event emitters. 147 | inherits(Request, EventEmitter); 148 | 149 | /** 150 | * Get a specific param by name or index. 151 | * 152 | * @param {number|string} The index or name of the param. 153 | * @return {object} The matched variables 154 | */ 155 | Request.prototype.param = function(indexOrName) { 156 | return this.params[indexOrName]; 157 | }; 158 | 159 | /** 160 | * Check whether the request triggered by one of the the specified causes. 161 | */ 162 | Request.prototype.from = function() { 163 | var causes; 164 | if (typeof arguments[0] === 'string') causes = arguments; 165 | else causes = arguments[0]; 166 | 167 | if (causes && causes.length) { 168 | for (var i = 0, len = causes.length; i < len; i++) { 169 | if (causes[i] === this.cause) return true; 170 | } 171 | } 172 | return false; 173 | }; 174 | 175 | Request.prototype.cancel = function() { 176 | if (!this.canceled) { 177 | this.canceled = true; 178 | this.emit('cancel', new Cancel(this)); 179 | } 180 | }; 181 | 182 | Request.prototype.canceled = false; 183 | 184 | module.exports = Request; 185 | 186 | },{"./errors/Cancel":7,"inherits":21,"query-string":22,"urllite":25,"urllite/lib/extensions/resolve":29,"wolfy87-eventemitter":31,"xtend/mutable":33}],3:[function(_dereq_,module,exports){ 187 | var inherits = _dereq_('inherits'); 188 | var Unhandled = _dereq_('./errors/Unhandled'); 189 | var EventEmitter = _dereq_('wolfy87-eventemitter'); 190 | var extend = _dereq_('xtend'); 191 | var delayed = _dereq_('./utils/delayed'); 192 | var parallel = _dereq_('run-parallel'); 193 | var withoutResults = _dereq_('./utils/withoutResults'); 194 | var thunkifyAll = _dereq_('./utils/thunkifyAll'); 195 | var noop = _dereq_('./utils/noop'); 196 | 197 | 198 | /** 199 | * The Response is used as the `this` value for route functions. It hosts 200 | * methods for manipulating the server response and application state. 201 | */ 202 | function Response(request, router) { 203 | this.request = request; 204 | this.router = router; 205 | this.state = router.state; 206 | this.vars = {}; 207 | this._beforeRenderHooks = []; 208 | 209 | request.on('cancel', this['throw'].bind(this)); 210 | } 211 | 212 | inherits(Response, EventEmitter); 213 | 214 | Response.prototype.status = 200; 215 | 216 | // 217 | // 218 | // The main methods for manipulating the application state. 219 | // 220 | // 221 | 222 | /** 223 | * A function decorator used to prevent methods from having any effect if the 224 | * corresponding request has been canceled. This prevents you from having to 225 | * check if the request was cancled every time you do something async in your 226 | * handler (however, if you're doing a lot of work, you should do an explicit 227 | * check and opt out of it). 228 | */ 229 | function unlessCanceled(fn) { 230 | return function() { 231 | if (this.request.canceled) return this; 232 | return fn.apply(this, arguments); 233 | }; 234 | } 235 | 236 | /** 237 | * Update the view vars for any subsequent views rendered by this response. 238 | */ 239 | Response.prototype.setVars = unlessCanceled(function(vars) { 240 | this.vars = extend(this.vars, vars); 241 | return this; 242 | }); 243 | 244 | Response.prototype.setState = unlessCanceled(function(state) { 245 | this.router.setState(state); 246 | this.state = this.router.state; 247 | return this; 248 | }); 249 | 250 | Response.prototype.setView = unlessCanceled(function(view) { 251 | this.view = view; 252 | return this; 253 | }); 254 | 255 | Response.prototype.beforeRender = function(fn) { 256 | this._beforeRenderHooks.push(fn); 257 | return this; 258 | }; 259 | 260 | // 261 | // 262 | // Ending 263 | // 264 | // 265 | 266 | /** 267 | * Indicates that a response was ended. 268 | */ 269 | Response.prototype.ended = false; 270 | Response.prototype.initialEnded = false; 271 | 272 | /** 273 | * Call to indicate that router is in the "initial" state—the state that the 274 | * server will send and the browser app will begin in. 275 | */ 276 | Response.prototype.endInitial = function() { 277 | if (this.initialEnded) { 278 | return; 279 | } 280 | this.initialEnded = true; 281 | this.emit('initialReady'); 282 | if (this.request.initialOnly) { 283 | this.end(); 284 | } 285 | return this.ended; 286 | }; 287 | 288 | Response.prototype.end = function() { 289 | if (!this.ended) { 290 | this.endInitial(); 291 | this.ended = true; 292 | this.emit('end'); 293 | } 294 | }; 295 | 296 | /** 297 | * A method for declaring that the router will not handle the current request. 298 | * Under normal circumstances, you shouldn't use this method but instead simply 299 | * call your handler's `done` callback: 300 | * 301 | * Router.route('users/:username', function(req, done) { 302 | * if (this.params.username === 'Matthew') { 303 | * // Do stuff 304 | * } else { 305 | * done(); 306 | * } 307 | * }); 308 | * 309 | * This allows other middleware the opportunity to handle the request. 310 | * 311 | * Calling this method is the same as invoking the handler's callback with an 312 | * `Unhandled` error in your route handler: 313 | * 314 | * Router.route('users/:username', function() { 315 | * if (this.params.username === 'Matthew') { 316 | * // Do stuff 317 | * } else { 318 | * throw new Router.Unhandled(this.request); 319 | * } 320 | * }); 321 | * 322 | * or (the async version) 323 | * 324 | * Router.route('users/:username', function(done) { 325 | * if (this.params.username === 'Matthew') { 326 | * // Do stuff 327 | * } else { 328 | * done(new Router.Unhandled(this.request)); 329 | * } 330 | * }); 331 | */ 332 | Response.prototype.unhandled = function(msg) { 333 | this['throw'](new Unhandled(this.request, msg)); 334 | }; 335 | 336 | Response.prototype['throw'] = function(err) { 337 | if (!this.error) { 338 | this.error = err; 339 | this.emit('error', err); 340 | } 341 | }; 342 | 343 | // 344 | // 345 | // Metadata methods. 346 | // 347 | // 348 | 349 | /** 350 | * Indicate that no view was found for the corresponding request. This has no 351 | * effect in the browser, but causes a 404 response to be sent on the server. 352 | * Note that this is different from `unhandled` as it indicates that the UI has 353 | * been updated. 354 | */ 355 | Response.prototype.notFound = function() { 356 | // TODO: Should this work as a getter too? Or should we make it chainable? 357 | this.status = 404; 358 | return this; 359 | }; 360 | 361 | Response.prototype.doctype = ''; 362 | Response.prototype.contentType = 'text/html; charset=utf-8'; 363 | 364 | // 365 | // 366 | // Rendering shortcuts. 367 | // 368 | // 369 | 370 | function bindVars(view, vars) { 371 | if (vars) { 372 | var oldView = view; 373 | return function(extraVars) { 374 | return oldView.call(this, extend(vars, extraVars)); 375 | }; 376 | } 377 | return view; 378 | } 379 | 380 | /** 381 | * A function decorator that creates a render function that accepts vars (as a 382 | * second argument) from one that doesn't. 383 | * 384 | * @param {function} fn The function whose arguments to transform. 385 | */ 386 | function renderer(fn) { 387 | return function() { 388 | var view, vars, cb, callback; 389 | var args = Array.prototype.slice.call(arguments, 0); 390 | if (typeof args[0] === 'function') 391 | view = args.shift(); 392 | if (typeof args[0] === 'object') 393 | vars = args.shift(); 394 | cb = args[0]; 395 | 396 | var boundRender = function(next) { 397 | // Pass a partially applied version of the view to the original function. 398 | vars = extend(this.vars, vars); 399 | view = bindVars(view || this.view, vars); 400 | fn.call(this, view, next); 401 | }; 402 | 403 | var hooks = thunkifyAll(this._beforeRenderHooks, this); 404 | parallel(hooks, function(err) { 405 | boundRender.call(this, cb || noop); 406 | }.bind(this)); 407 | }; 408 | } 409 | 410 | // 411 | // Shortcut methods for rendering a view with props and (optionally) ending the 412 | // request. 413 | // 414 | 415 | Response.prototype._renderIntermediate = unlessCanceled(function(view, cb) { 416 | delayed(function() { 417 | this.router.setView(view); 418 | cb.call(this); 419 | }.bind(this))(); 420 | }); 421 | 422 | /** 423 | * Render the provided view and end the request. 424 | */ 425 | Response.prototype.render = renderer(function(view, cb) { 426 | this._renderIntermediate(view, function(err) { 427 | if (err) { 428 | cb(err); 429 | return; 430 | } 431 | cb.apply(this, arguments); 432 | this.end(); 433 | }); 434 | }); 435 | 436 | /** 437 | * Render the provided view and mark the current state as the initial one. 438 | */ 439 | Response.prototype.renderInitial = renderer(function(view, cb) { 440 | this._renderIntermediate(view, function(err) { 441 | if (err) { 442 | cb(err); 443 | return; 444 | } 445 | this.endInitial(); 446 | if (!this.request.initialOnly) cb.apply(this, arguments); 447 | }); 448 | }); 449 | 450 | /** 451 | * Render the provided view. 452 | */ 453 | Response.prototype.renderIntermediate = renderer(function(view, cb) { 454 | this._renderIntermediate(view, cb); 455 | }); 456 | 457 | Response.prototype.renderDocumentToString = function() { 458 | var engine = this.router.constructor.engine; 459 | var markup = engine.renderToString(this.router); 460 | return (this.doctype || '') + markup; 461 | }; 462 | 463 | module.exports = Response; 464 | 465 | },{"./errors/Unhandled":8,"./utils/delayed":16,"./utils/noop":17,"./utils/thunkifyAll":19,"./utils/withoutResults":20,"inherits":21,"run-parallel":23,"wolfy87-eventemitter":31,"xtend":32}],4:[function(_dereq_,module,exports){ 466 | var pathToRegexp = _dereq_('./utils/pathToRegexp'); 467 | 468 | 469 | function Route(path) { 470 | this.path = path; 471 | this.keys = []; 472 | this.tokens = []; 473 | this.regexp = pathToRegexp(path, this.keys, this.tokens, {strict: true}); 474 | } 475 | 476 | Route.prototype.match = function(url) { 477 | var match = url.match(this.regexp); 478 | if (!match) return; 479 | var matchObj = {}; 480 | for (var i = 1, len = match.length; i < len; i++) { 481 | matchObj[i] = matchObj[this.keys[i - 1].name] = match[i]; 482 | } 483 | return matchObj; 484 | }; 485 | 486 | Route.prototype.url = function(params) { 487 | return this.tokens.map(function(token) { 488 | if (token.literal != null) return token.literal; 489 | if (token.name) { 490 | if (params && params[token.name] != null) 491 | return token.delimiter + params[token.name]; 492 | else if (token.optional) 493 | return ''; 494 | throw new Error('Missing required param "' + token.name + '"'); 495 | } 496 | }).join(''); 497 | }; 498 | 499 | module.exports = Route; 500 | 501 | },{"./utils/pathToRegexp":18}],5:[function(_dereq_,module,exports){ 502 | var extend = _dereq_('xtend'); 503 | var Route = _dereq_('./Route'); 504 | var Request = _dereq_('./Request'); 505 | var Response = _dereq_('./Response'); 506 | var Unhandled = _dereq_('./errors/Unhandled'); 507 | var delayed = _dereq_('./utils/delayed'); 508 | var getDefaultHistory = _dereq_('./history/getHistory'); 509 | var series = _dereq_('run-series'); 510 | var inherits = _dereq_('inherits'); 511 | var EventEmitter = _dereq_('wolfy87-eventemitter'); 512 | var attach = _dereq_('./attach'); 513 | var LinkHijacker = _dereq_('./LinkHijacker'); 514 | var withoutResults = _dereq_('./utils/withoutResults'); 515 | var thunkifyAll = _dereq_('./utils/thunkifyAll'); 516 | 517 | 518 | function Router(opts) { 519 | if (opts) { 520 | this.state = extend(opts.initialState); 521 | this.history = opts.history; 522 | } 523 | this.url = this.constructor.url.bind(this.constructor); 524 | // TODO: Should we add the properties from the class (i.e. rootUrl) to the instance? 525 | } 526 | 527 | inherits(Router, EventEmitter); 528 | 529 | 530 | /** 531 | * Expose the `Unhandled` error so it can be used in handlers more easily. 532 | */ 533 | Router.Unhandled = Unhandled; 534 | 535 | function throwNoEngine() { 536 | throw new Error('You are attempting to render but your router has no rendering engine.'); 537 | } 538 | 539 | Router.engine = { 540 | renderInto: throwNoEngine, 541 | renderToString: throwNoEngine 542 | }; 543 | 544 | /** 545 | * Add a route to the router class. This API can be used in a manner similar 546 | * to many server-side JS routing libraries—by chainging calls to `route`— 547 | * however, it's more likely that you'll want to pass your routes to the 548 | * router class constructor using JSX. 549 | * 550 | * This function accepts either `(path, handlers...)`, or 551 | * `(name, path, handlers...)`. 552 | */ 553 | Router.route = function() { 554 | var RouterClass = this; 555 | var args = Array.prototype.slice.call(arguments, 0); 556 | var name, path, handlers; 557 | 558 | if (typeof args[1] !== 'function') name = args.shift(); 559 | path = args.shift(); 560 | handlers = args; 561 | 562 | var route = new Route(path); 563 | 564 | if (name) { 565 | if (RouterClass.namedRoutes[name]) 566 | throw new Error('Route with name "' + name + '" already exists.'); 567 | RouterClass.namedRoutes[name] = route; 568 | } 569 | 570 | // Create and register a middleware to represent this route. 571 | RouterClass.use(function(req, next) { 572 | // If this route doesn't match, skip it. 573 | var match = route.match(req.path); 574 | if (!match) return next(); 575 | 576 | req.params = match; 577 | 578 | series(thunkifyAll(handlers, this, [req]), withoutResults(next)); 579 | }); 580 | 581 | // For chaining! 582 | return this; 583 | }; 584 | 585 | Router.prototype.setState = function(state) { 586 | this.state = extend(this.state, state); 587 | this.emit('stateChange'); 588 | }; 589 | 590 | Router.prototype.setView = function(view) { 591 | this.view = view; 592 | this.emit('viewChange'); 593 | }; 594 | 595 | Router.prototype.render = function() { 596 | if (!this.view) { 597 | throw new Error('You must set a view before rendering'); 598 | } 599 | return this.view(extend(this.state)); 600 | }; 601 | 602 | Router.finalMiddleware = [ 603 | // If we've exhausted the middleware without handling the request, call the 604 | // `unhandled()` method. Going through `unhandled` instead of just creating an 605 | // error in the dispatch callback means we're always going through the same 606 | // process, getting the same events, etc. 607 | function(req, next) { 608 | if (!this.error && !this.ended) return this.unhandled(); 609 | next(); 610 | } 611 | ]; 612 | 613 | /** 614 | * 615 | * @param {string} url 616 | * @param {function?} callback 617 | */ 618 | Router.prototype.dispatch = function(url, opts, callback) { 619 | var RouterClass = this.constructor; 620 | 621 | if (typeof arguments[1] === 'function') { 622 | callback = arguments[1]; 623 | opts = null; 624 | } 625 | 626 | // Wrap the callback, imposing a delay to force asynchronousness in 627 | // case the user calls it synchronously. 628 | var cb = function(err) { 629 | // Clean up listeners 630 | res.removeAllListeners(); 631 | req.removeAllListeners(); 632 | 633 | this._currentResponse = null; 634 | if (callback) callback(err, err ? null : res); 635 | }.bind(this); 636 | 637 | var first = (opts && opts.first) || !this._dispatchedFirstRequest; 638 | var req = new Request(url, extend(opts, { 639 | cause: opts && opts.cause, 640 | first: first, 641 | root: RouterClass.rootUrl 642 | })); 643 | this._dispatchedFirstRequest = true; 644 | 645 | var res = new Response(req, this) 646 | .on('error', cb) 647 | .on('end', cb); 648 | 649 | if (req.url == null) { 650 | delayed(function() { 651 | res.unhandled('URL not within router root: ' + url); 652 | })(); 653 | return res; 654 | } 655 | 656 | if (this._currentResponse) { 657 | this._currentResponse.request.cancel(); 658 | } 659 | this._currentResponse = res; 660 | 661 | // Force async behavior so you have a chance to add listeners to the 662 | // request object. 663 | var middleware = RouterClass.middleware.concat(RouterClass.finalMiddleware); 664 | delayed(function() { 665 | series(thunkifyAll(middleware, res, [req]), function(err) { 666 | if (err) { 667 | // Create a wrapper for each of the error middlewares that receives the 668 | // error from the previous one. This way, error middleware can pass a 669 | // different error than it received, and this new error can be handled 670 | // by subsequent error middleware. If an error middleware does not pass 671 | // an error or end the request, the request will be considered 672 | // unhandled. 673 | var previousError = err; 674 | var errorMiddleware = RouterClass.errorMiddleware.map(function(fn) { 675 | return function(done) { 676 | var newDone = function(err, result) { 677 | if (err) { 678 | previousError = err; 679 | done(); 680 | } else { 681 | res.unhandled(); 682 | } 683 | }; 684 | try { 685 | fn.call(res, previousError, req, newDone); 686 | } catch (err) { 687 | newDone(err); 688 | } 689 | }; 690 | }); 691 | series(errorMiddleware, function() { 692 | if (previousError) res['throw'](previousError); 693 | }); 694 | } 695 | }); 696 | })(); 697 | 698 | return res; 699 | }; 700 | 701 | Router.prototype.captureClicks = function(el, opts) { 702 | return new LinkHijacker(this, el, opts); 703 | }; 704 | 705 | Router.use = function(middleware) { 706 | var RouterClass = this, 707 | middlewares = middleware.length === 3 ? RouterClass.errorMiddleware : RouterClass.middleware; 708 | middlewares.push(middleware); 709 | return this; 710 | }; 711 | 712 | Router.extend = function(opts) { 713 | var SuperClass = this; 714 | var NewRouter = function(opts) { 715 | if (!(this instanceof NewRouter)) { 716 | return new NewRouter(opts); 717 | } 718 | SuperClass.call(this, opts); 719 | }; 720 | inherits(NewRouter, SuperClass); 721 | 722 | // Add "static" props to the new router. 723 | for (var k in SuperClass) { 724 | if (SuperClass.hasOwnProperty(k)) { 725 | if (k === 'super_' || k === 'prototype') continue; 726 | NewRouter[k] = SuperClass[k]; 727 | } 728 | } 729 | 730 | NewRouter.engine = opts && opts.engine; 731 | NewRouter.rootUrl = opts && opts.rootUrl || ''; 732 | NewRouter.middleware = []; 733 | NewRouter.errorMiddleware = []; 734 | NewRouter.namedRoutes = {}; 735 | 736 | return NewRouter; 737 | }; 738 | 739 | Router.attach = function(element, opts) { 740 | return attach(this, element, opts); 741 | }; 742 | 743 | /** 744 | * Extensions are just functions that mutate the router in any way they want 745 | * and return it. This function is just a prettier way to use them than calling 746 | * them directly. 747 | */ 748 | Router.setup = function(extension) { 749 | var router = extension(this); 750 | if (!router) 751 | throw new Error('Invalid extension: extension did not return router.'); 752 | return router; 753 | }; 754 | 755 | Router.url = function(name, params) { 756 | var route = this.namedRoutes[name]; 757 | if (!route) 758 | throw new Error('There is no route named "' + name + '".'); 759 | return route.url(params); 760 | }; 761 | 762 | module.exports = Router; 763 | 764 | },{"./LinkHijacker":1,"./Request":2,"./Response":3,"./Route":4,"./attach":6,"./errors/Unhandled":8,"./history/getHistory":15,"./utils/delayed":16,"./utils/thunkifyAll":19,"./utils/withoutResults":20,"inherits":21,"run-series":24,"wolfy87-eventemitter":31,"xtend":32}],6:[function(_dereq_,module,exports){ 765 | var getDefaultHistory = _dereq_('./history/getHistory'); 766 | 767 | 768 | /** 769 | * Bootstraps the app by getting the initial state. 770 | */ 771 | function attach(Router, element, opts) { 772 | if (!opts) opts = {}; 773 | var history = opts.history || getDefaultHistory(); 774 | var router = new Router({history: history}); 775 | 776 | var render = function() { 777 | Router.engine.renderInto(router, element); 778 | }; 779 | 780 | var onInitialReady = function() { 781 | render(); 782 | router 783 | .on('viewChange', render) 784 | .on('stateChange', render); 785 | 786 | // Now that the view has been bootstrapped (i.e. is in its inital state), it 787 | // can be updated. 788 | update(); 789 | history.on('update', function(meta) { 790 | update(meta); 791 | }); 792 | }; 793 | 794 | var previousURL; 795 | var update = function(meta) { 796 | var url = history.currentURL(); 797 | if (url === previousURL) return; 798 | previousURL = url; 799 | 800 | var res = router.dispatch(url, meta, function(err) { 801 | if (err && (err.name !== 'Unhandled') && (err.name !== 'Cancel')) { 802 | throw err; 803 | } 804 | }); 805 | 806 | if (meta && meta.cause === 'startup') { 807 | res.once('initialReady', onInitialReady); 808 | } 809 | }; 810 | 811 | // Start the process. 812 | update({cause: 'startup'}); 813 | 814 | return router; 815 | } 816 | 817 | module.exports = attach; 818 | 819 | },{"./history/getHistory":15}],7:[function(_dereq_,module,exports){ 820 | var initError = _dereq_('./initError'); 821 | 822 | function Cancel(request) { 823 | initError(this, 'Cancel', 'Request was canceled: ' + request.path); 824 | this.request = request; 825 | } 826 | 827 | Cancel.prototype = Error.prototype; 828 | 829 | module.exports = Cancel; 830 | 831 | },{"./initError":9}],8:[function(_dereq_,module,exports){ 832 | var initError = _dereq_('./initError'); 833 | 834 | function Unhandled(request, msg) { 835 | if (!msg) msg = 'Path not found: ' + request.path; 836 | initError(this, 'Unhandled', msg); 837 | this.request = request; 838 | } 839 | 840 | Unhandled.prototype = Error.prototype; 841 | 842 | module.exports = Unhandled; 843 | 844 | },{"./initError":9}],9:[function(_dereq_,module,exports){ 845 | function initError(error, name, msg) { 846 | var source = new Error(msg); 847 | error.name = source.name = name; 848 | error.message = source.message; 849 | 850 | if (source.stack) { 851 | error.stack = source.stack; 852 | } 853 | 854 | error.toString = function() { 855 | return this.name + ': ' + this.message; 856 | } 857 | }; 858 | 859 | module.exports = initError; 860 | 861 | },{}],10:[function(_dereq_,module,exports){ 862 | var Router = _dereq_('./Router'); 863 | 864 | function monorouter(opts) { 865 | return Router.extend(opts); 866 | } 867 | 868 | module.exports = monorouter; 869 | 870 | },{"./Router":5}],11:[function(_dereq_,module,exports){ 871 | var inherits = _dereq_('inherits'); 872 | var EventEmitter = _dereq_('wolfy87-eventemitter'); 873 | 874 | 875 | function BaseHistory() {} 876 | 877 | // Make BaseHistory an event emitter. 878 | inherits(BaseHistory, EventEmitter); 879 | 880 | /** 881 | * Navigate to the provided URL without creating a duplicate history entry if 882 | * you're already there. 883 | */ 884 | BaseHistory.prototype.navigate = function(url, meta) { 885 | if (url !== this.currentURL()) { 886 | this.push(url, meta); 887 | } else { 888 | this.replace(url, meta); 889 | } 890 | }; 891 | 892 | module.exports = BaseHistory; 893 | 894 | },{"inherits":21,"wolfy87-eventemitter":31}],12:[function(_dereq_,module,exports){ 895 | var inherits = _dereq_('inherits'); 896 | var BaseHistory = _dereq_('./BaseHistory'); 897 | var urllite = _dereq_ ('urllite'); 898 | 899 | 900 | /** 901 | * A history interface for browsers that don't support pushState. 902 | */ 903 | function FallbackHistory() {} 904 | 905 | inherits(FallbackHistory, BaseHistory); 906 | 907 | FallbackHistory.prototype.currentURL = function() { 908 | // If we have our own idea of the URL, use that. 909 | if (this._url) return this._url; 910 | 911 | // Use urllite to pave over IE issues with pathname. 912 | var parsed = urllite(document.location.href); 913 | return parsed.pathname + parsed.search + parsed.hash; 914 | }; 915 | 916 | FallbackHistory.prototype.push = function(path) { 917 | // No need to update `this._url`—this code is all going to be reloaded. 918 | window.location = path; 919 | }; 920 | 921 | FallbackHistory.prototype.replace = function(path, meta) { 922 | // For the fallback history, `replace` won't actually change the browser 923 | // address, but will update its own URL. This is because `replace` usually 924 | // corresponds to "lesser" state changes: having a stale browser URL is 925 | // considered more acceptable than refreshing the entire page. 926 | this._url = path; 927 | this.emit('update', meta); 928 | }; 929 | 930 | module.exports = FallbackHistory; 931 | 932 | },{"./BaseHistory":11,"inherits":21,"urllite":25}],13:[function(_dereq_,module,exports){ 933 | var PushStateHistory = _dereq_('./PushStateHistory'); 934 | var FallbackHistory = _dereq_('./FallbackHistory'); 935 | 936 | 937 | var win = typeof window !== 'undefined' ? window : null; 938 | var history = win && win.history; 939 | 940 | module.exports = history && history.pushState ? PushStateHistory : FallbackHistory; 941 | 942 | },{"./FallbackHistory":12,"./PushStateHistory":14}],14:[function(_dereq_,module,exports){ 943 | var inherits = _dereq_('inherits'); 944 | var BaseHistory = _dereq_('./BaseHistory'); 945 | var urllite = _dereq_ ('urllite'); 946 | 947 | 948 | /** 949 | * A history implementation that uses `pushState`. 950 | */ 951 | function PushStateHistory() { 952 | window.addEventListener('popstate', function(event) { 953 | this.emit('update', {cause: 'popstate'}); 954 | }.bind(this)); 955 | } 956 | 957 | inherits(PushStateHistory, BaseHistory); 958 | 959 | PushStateHistory.prototype.currentURL = function() { 960 | // Use urllite to pave over IE issues with pathname. 961 | var parsed = urllite(document.location.href); 962 | return parsed.pathname + parsed.search + parsed.hash; 963 | }; 964 | 965 | PushStateHistory.prototype.push = function(path, meta) { 966 | window.history.pushState({}, '', path); 967 | this.emit('update', meta); 968 | }; 969 | 970 | PushStateHistory.prototype.replace = function(path, meta) { 971 | window.history.replaceState({}, '', path); 972 | this.emit('update', meta); 973 | }; 974 | 975 | module.exports = PushStateHistory; 976 | 977 | },{"./BaseHistory":11,"inherits":21,"urllite":25}],15:[function(_dereq_,module,exports){ 978 | var History = _dereq_('./History'); 979 | 980 | var singleton; 981 | 982 | function getHistory() { 983 | if (!singleton) 984 | singleton = new History(); 985 | return singleton; 986 | } 987 | 988 | module.exports = getHistory; 989 | 990 | },{"./History":13}],16:[function(_dereq_,module,exports){ 991 | var delay = typeof setImmediate === 'function' ? setImmediate : function(fn) { 992 | setTimeout(fn, 0); 993 | }; 994 | 995 | /** 996 | * Creates a delayed version of the provided function. This is used to guarantee 997 | * ansynchronous behavior for potentially synchronous operations. 998 | */ 999 | function delayed(fn) { 1000 | return function() { 1001 | var args = arguments; 1002 | var self = this; 1003 | var fnWithArgs = function() { 1004 | fn.apply(self, args); 1005 | }; 1006 | delay(fnWithArgs); 1007 | }; 1008 | } 1009 | 1010 | module.exports = delayed; 1011 | 1012 | },{}],17:[function(_dereq_,module,exports){ 1013 | module.exports = function() {}; 1014 | 1015 | },{}],18:[function(_dereq_,module,exports){ 1016 | // A custom version of Blake Embrey's path-to-regexp—modified in order to 1017 | // support URL reversal. 1018 | 1019 | /* 1020 | * The MIT License (MIT) 1021 | * 1022 | * Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) 1023 | * 1024 | * Permission is hereby granted, free of charge, to any person obtaining a copy 1025 | * of this software and associated documentation files (the "Software"), to deal 1026 | * in the Software without restriction, including without limitation the rights 1027 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 1028 | * copies of the Software, and to permit persons to whom the Software is 1029 | * furnished to do so, subject to the following conditions: 1030 | * 1031 | * The above copyright notice and this permission notice shall be included in 1032 | * all copies or substantial portions of the Software. 1033 | * 1034 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 1035 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 1036 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 1037 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 1038 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 1039 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 1040 | * THE SOFTWARE. 1041 | */ 1042 | 1043 | /** 1044 | * Expose `pathtoRegexp`. 1045 | */ 1046 | module.exports = pathtoRegexp; 1047 | 1048 | var PATH_REGEXP = new RegExp([ 1049 | // Match already escaped characters that would otherwise incorrectly appear 1050 | // in future matches. This allows the user to escape special characters that 1051 | // shouldn't be transformed. 1052 | '(\\\\.)', 1053 | // Match Express-style parameters and un-named parameters with a prefix 1054 | // and optional suffixes. Matches appear as: 1055 | // 1056 | // "/:test(\\d+)?" => ["/", "test", "\d+", undefined, "?"] 1057 | // "/route(\\d+)" => [undefined, undefined, undefined, "\d+", undefined] 1058 | '([\\/.])?(?:\\:(\\w+)(?:\\(((?:\\\\.|[^)])*)\\))?|\\(((?:\\\\.|[^)])*)\\))([+*?])?', 1059 | // Match regexp special characters that should always be escaped. 1060 | '([.+*?=^!:${}()[\\]|\\/])' 1061 | ].join('|'), 'g'); 1062 | 1063 | /** 1064 | * Escape the capturing group by escaping special characters and meaning. 1065 | * 1066 | * @param {String} group 1067 | * @return {String} 1068 | */ 1069 | function escapeGroup (group) { 1070 | return group.replace(/([=!:$\/()])/g, '\\$1'); 1071 | } 1072 | 1073 | /** 1074 | * Normalize the given path string, returning a regular expression. 1075 | * 1076 | * An empty array should be passed in, which will contain the placeholder key 1077 | * names. For example `/user/:id` will then contain `["id"]`. 1078 | * 1079 | * @param {(String|RegExp|Array)} path 1080 | * @param {Array} keys 1081 | * @param {Array} tokens 1082 | * @param {Object} options 1083 | * @return {RegExp} 1084 | */ 1085 | function pathtoRegexp (path, keys, tokens, options) { 1086 | keys = keys || []; 1087 | tokens = tokens || []; 1088 | options = options || {}; 1089 | 1090 | var strict = options.strict; 1091 | var end = options.end !== false; 1092 | var flags = options.sensitive ? '' : 'i'; 1093 | var index = 0; 1094 | 1095 | var charIndex = 0; 1096 | var originalPath = path; 1097 | 1098 | // Alter the path string into a usable regexp. 1099 | path = path.replace(PATH_REGEXP, function (match, escaped, prefix, key, capture, group, suffix, escape, offset) { 1100 | 1101 | if (offset !== charIndex) { 1102 | tokens.push({literal: path.slice(charIndex, offset)}); 1103 | } 1104 | charIndex = offset + match.length; 1105 | 1106 | // Avoiding re-escaping escaped characters. 1107 | if (escaped) { 1108 | return escaped; 1109 | } 1110 | 1111 | // Escape regexp special characters. 1112 | if (escape) { 1113 | tokens.push({literal: escape}); 1114 | return '\\' + escape; 1115 | } 1116 | 1117 | var repeat = suffix === '+' || suffix === '*'; 1118 | var optional = suffix === '?' || suffix === '*'; 1119 | 1120 | keys.push({ 1121 | name: key || index++, 1122 | delimiter: prefix || '/', 1123 | optional: optional, 1124 | repeat: repeat 1125 | }); 1126 | tokens.push(keys[keys.length - 1]); 1127 | 1128 | // Escape the prefix character. 1129 | prefix = prefix ? '\\' + prefix : ''; 1130 | 1131 | // Match using the custom capturing group, or fallback to capturing 1132 | // everything up to the next slash (or next period if the param was 1133 | // prefixed with a period). 1134 | capture = escapeGroup(capture || group || '[^' + (prefix || '\\/') + ']+?'); 1135 | 1136 | // Allow parameters to be repeated more than once. 1137 | if (repeat) { 1138 | capture = capture + '(?:' + prefix + capture + ')*'; 1139 | } 1140 | 1141 | // Allow a parameter to be optional. 1142 | if (optional) { 1143 | return '(?:' + prefix + '(' + capture + '))?'; 1144 | } 1145 | 1146 | // Basic parameter support. 1147 | return prefix + '(' + capture + ')'; 1148 | }); 1149 | 1150 | // Add any unconsumed part of the string to our token list. 1151 | if (charIndex !== originalPath.length) { 1152 | tokens.push({literal: originalPath.slice(charIndex, originalPath.length)}); 1153 | } 1154 | 1155 | // Check whether the path ends in a slash as it alters some match behaviour. 1156 | var endsWithSlash = path[path.length - 1] === '/'; 1157 | 1158 | // In non-strict mode we allow an optional trailing slash in the match. If 1159 | // the path to match already ended with a slash, we need to remove it for 1160 | // consistency. The slash is only valid at the very end of a path match, not 1161 | // anywhere in the middle. This is important for non-ending mode, otherwise 1162 | // "/test/" will match "/test//route". 1163 | if (!strict) { 1164 | path = (endsWithSlash ? path.slice(0, -2) : path) + '(?:\\/(?=$))?'; 1165 | } 1166 | 1167 | // In non-ending mode, we need prompt the capturing groups to match as much 1168 | // as possible by using a positive lookahead for the end or next path segment. 1169 | if (!end) { 1170 | path += strict && endsWithSlash ? '' : '(?=\\/|$)'; 1171 | } 1172 | 1173 | return new RegExp('^' + path + (end ? '$' : ''), flags); 1174 | } 1175 | 1176 | },{}],19:[function(_dereq_,module,exports){ 1177 | /** 1178 | * Return thunkified versions of the provided functions bound to the specified 1179 | * context and with the provided initial args. 1180 | */ 1181 | function thunkifyAll(fns, thisArg, args) { 1182 | fns = fns || []; 1183 | return fns.map(function(fn) { 1184 | return function(done) { 1185 | var newArgs = args ? Array.prototype.slice.call(args, 0) : []; 1186 | newArgs.push(done); 1187 | try { 1188 | fn.apply(thisArg, newArgs); 1189 | } catch (err) { 1190 | done(err); 1191 | } 1192 | }; 1193 | }); 1194 | } 1195 | 1196 | module.exports = thunkifyAll; 1197 | 1198 | },{}],20:[function(_dereq_,module,exports){ 1199 | /** 1200 | * Creates a new version of a callback that doesn't get the results. This is 1201 | * used so that we don't forward collected results from run-series. 1202 | */ 1203 | function withoutResults(callback, thisArg) { 1204 | if (callback) { 1205 | return function(err, results) { 1206 | return callback.call(thisArg, err); 1207 | }; 1208 | } 1209 | } 1210 | 1211 | module.exports = withoutResults; 1212 | 1213 | },{}],21:[function(_dereq_,module,exports){ 1214 | if (typeof Object.create === 'function') { 1215 | // implementation from standard node.js 'util' module 1216 | module.exports = function inherits(ctor, superCtor) { 1217 | ctor.super_ = superCtor 1218 | ctor.prototype = Object.create(superCtor.prototype, { 1219 | constructor: { 1220 | value: ctor, 1221 | enumerable: false, 1222 | writable: true, 1223 | configurable: true 1224 | } 1225 | }); 1226 | }; 1227 | } else { 1228 | // old school shim for old browsers 1229 | module.exports = function inherits(ctor, superCtor) { 1230 | ctor.super_ = superCtor 1231 | var TempCtor = function () {} 1232 | TempCtor.prototype = superCtor.prototype 1233 | ctor.prototype = new TempCtor() 1234 | ctor.prototype.constructor = ctor 1235 | } 1236 | } 1237 | 1238 | },{}],22:[function(_dereq_,module,exports){ 1239 | /*! 1240 | query-string 1241 | Parse and stringify URL query strings 1242 | https://github.com/sindresorhus/query-string 1243 | by Sindre Sorhus 1244 | MIT License 1245 | */ 1246 | (function () { 1247 | 'use strict'; 1248 | var queryString = {}; 1249 | 1250 | queryString.parse = function (str) { 1251 | if (typeof str !== 'string') { 1252 | return {}; 1253 | } 1254 | 1255 | str = str.trim().replace(/^(\?|#)/, ''); 1256 | 1257 | if (!str) { 1258 | return {}; 1259 | } 1260 | 1261 | return str.trim().split('&').reduce(function (ret, param) { 1262 | var parts = param.replace(/\+/g, ' ').split('='); 1263 | var key = parts[0]; 1264 | var val = parts[1]; 1265 | 1266 | key = decodeURIComponent(key); 1267 | // missing `=` should be `null`: 1268 | // http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters 1269 | val = val === undefined ? null : decodeURIComponent(val); 1270 | 1271 | if (!ret.hasOwnProperty(key)) { 1272 | ret[key] = val; 1273 | } else if (Array.isArray(ret[key])) { 1274 | ret[key].push(val); 1275 | } else { 1276 | ret[key] = [ret[key], val]; 1277 | } 1278 | 1279 | return ret; 1280 | }, {}); 1281 | }; 1282 | 1283 | queryString.stringify = function (obj) { 1284 | return obj ? Object.keys(obj).map(function (key) { 1285 | var val = obj[key]; 1286 | 1287 | if (Array.isArray(val)) { 1288 | return val.map(function (val2) { 1289 | return encodeURIComponent(key) + '=' + encodeURIComponent(val2); 1290 | }).join('&'); 1291 | } 1292 | 1293 | return encodeURIComponent(key) + '=' + encodeURIComponent(val); 1294 | }).join('&') : ''; 1295 | }; 1296 | 1297 | if (typeof define === 'function' && define.amd) { 1298 | define(function() { return queryString; }); 1299 | } else if (typeof module !== 'undefined' && module.exports) { 1300 | module.exports = queryString; 1301 | } else { 1302 | window.queryString = queryString; 1303 | } 1304 | })(); 1305 | 1306 | },{}],23:[function(_dereq_,module,exports){ 1307 | module.exports = function (tasks, cb) { 1308 | var results, pending, keys 1309 | if (Array.isArray(tasks)) { 1310 | results = [] 1311 | pending = tasks.length 1312 | } else { 1313 | keys = Object.keys(tasks) 1314 | results = {} 1315 | pending = keys.length 1316 | } 1317 | 1318 | function done (i, err, result) { 1319 | results[i] = result 1320 | if (--pending === 0 || err) { 1321 | cb && cb(err, results) 1322 | cb = null 1323 | } 1324 | } 1325 | 1326 | if (!pending) { 1327 | // empty 1328 | cb && cb(null, results) 1329 | cb = null 1330 | } else if (keys) { 1331 | // object 1332 | keys.forEach(function (key) { 1333 | tasks[key](done.bind(undefined, key)) 1334 | }) 1335 | } else { 1336 | // array 1337 | tasks.forEach(function (task, i) { 1338 | task(done.bind(undefined, i)) 1339 | }) 1340 | } 1341 | } 1342 | 1343 | },{}],24:[function(_dereq_,module,exports){ 1344 | module.exports = function (tasks, cb) { 1345 | var current = 0 1346 | var results = [] 1347 | cb = cb || function () {} 1348 | 1349 | function done (err, result) { 1350 | if (err) return cb(err, results) 1351 | results.push(result) 1352 | 1353 | if (++current >= tasks.length) { 1354 | cb(null, results) 1355 | } else { 1356 | tasks[current](done) 1357 | } 1358 | } 1359 | 1360 | if (tasks.length) { 1361 | tasks[0](done) 1362 | } else { 1363 | cb(null, []) 1364 | } 1365 | } 1366 | 1367 | },{}],25:[function(_dereq_,module,exports){ 1368 | (function() { 1369 | var urllite; 1370 | 1371 | urllite = _dereq_('./core'); 1372 | 1373 | _dereq_('./extensions/resolve'); 1374 | 1375 | _dereq_('./extensions/relativize'); 1376 | 1377 | _dereq_('./extensions/normalize'); 1378 | 1379 | _dereq_('./extensions/toString'); 1380 | 1381 | module.exports = urllite; 1382 | 1383 | }).call(this); 1384 | 1385 | },{"./core":26,"./extensions/normalize":27,"./extensions/relativize":28,"./extensions/resolve":29,"./extensions/toString":30}],26:[function(_dereq_,module,exports){ 1386 | (function() { 1387 | var URL, URL_PATTERN, defaults, urllite, 1388 | __hasProp = {}.hasOwnProperty, 1389 | __slice = [].slice; 1390 | 1391 | URL_PATTERN = /^(?:(?:([^:\/?\#]+:)\/+|(\/\/))(?:([a-z0-9-\._~%]+)(?::([a-z0-9-\._~%]+))?@)?(([a-z0-9-\._~%!$&'()*+,;=]+)(?::([0-9]+))?)?)?([^?\#]*?)(\?[^\#]*)?(\#.*)?$/; 1392 | 1393 | urllite = function(raw, opts) { 1394 | return urllite.URL.parse(raw, opts); 1395 | }; 1396 | 1397 | urllite.URL = URL = (function() { 1398 | function URL(props) { 1399 | var k, v; 1400 | for (k in props) { 1401 | if (!__hasProp.call(props, k)) continue; 1402 | v = props[k]; 1403 | this[k] = v; 1404 | } 1405 | } 1406 | 1407 | URL.parse = function(raw) { 1408 | var m, pathname, protocol; 1409 | m = raw.toString().match(URL_PATTERN); 1410 | pathname = m[8] || ''; 1411 | protocol = m[1]; 1412 | return urllite._createURL({ 1413 | protocol: protocol, 1414 | username: m[3], 1415 | password: m[4], 1416 | hostname: m[6], 1417 | port: m[7], 1418 | pathname: protocol && pathname.charAt(0) !== '/' ? "/" + pathname : pathname, 1419 | search: m[9], 1420 | hash: m[10], 1421 | isSchemeRelative: m[2] != null 1422 | }); 1423 | }; 1424 | 1425 | return URL; 1426 | 1427 | })(); 1428 | 1429 | defaults = { 1430 | protocol: '', 1431 | username: '', 1432 | password: '', 1433 | host: '', 1434 | hostname: '', 1435 | port: '', 1436 | pathname: '', 1437 | search: '', 1438 | hash: '', 1439 | origin: '', 1440 | isSchemeRelative: false 1441 | }; 1442 | 1443 | urllite._createURL = function() { 1444 | var base, bases, k, props, v, _i, _len, _ref, _ref1; 1445 | bases = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 1446 | props = {}; 1447 | for (_i = 0, _len = bases.length; _i < _len; _i++) { 1448 | base = bases[_i]; 1449 | for (k in defaults) { 1450 | if (!__hasProp.call(defaults, k)) continue; 1451 | v = defaults[k]; 1452 | props[k] = (_ref = (_ref1 = base[k]) != null ? _ref1 : props[k]) != null ? _ref : v; 1453 | } 1454 | } 1455 | props.host = props.hostname && props.port ? "" + props.hostname + ":" + props.port : props.hostname ? props.hostname : ''; 1456 | props.origin = props.protocol ? "" + props.protocol + "//" + props.host : ''; 1457 | props.isAbsolutePathRelative = !props.host && props.pathname.charAt(0) === '/'; 1458 | props.isPathRelative = !props.host && props.pathname.charAt(0) !== '/'; 1459 | props.isRelative = props.isSchemeRelative || props.isAbsolutePathRelative || props.isPathRelative; 1460 | props.isAbsolute = !props.isRelative; 1461 | return new urllite.URL(props); 1462 | }; 1463 | 1464 | module.exports = urllite; 1465 | 1466 | }).call(this); 1467 | 1468 | },{}],27:[function(_dereq_,module,exports){ 1469 | (function() { 1470 | var URL, urllite; 1471 | 1472 | urllite = _dereq_('../core'); 1473 | 1474 | URL = urllite.URL; 1475 | 1476 | URL.prototype.normalize = function() { 1477 | var m, pathname; 1478 | pathname = this.pathname; 1479 | while (m = /^(.*?)[^\/]+\/\.\.\/*(.*)$/.exec(pathname)) { 1480 | pathname = "" + m[1] + m[2]; 1481 | } 1482 | if (this.host && pathname.indexOf('..') !== -1) { 1483 | throw new Error('Path is behind root.'); 1484 | } 1485 | return urllite._createURL(this, { 1486 | pathname: pathname 1487 | }); 1488 | }; 1489 | 1490 | }).call(this); 1491 | 1492 | },{"../core":26}],28:[function(_dereq_,module,exports){ 1493 | (function() { 1494 | var URL, urllite; 1495 | 1496 | urllite = _dereq_('../core'); 1497 | 1498 | _dereq_('./resolve'); 1499 | 1500 | URL = urllite.URL; 1501 | 1502 | URL.prototype.relativize = function(other) { 1503 | var c, i, newSegments, otherSegments, url, urlSegments, _i, _len, _ref; 1504 | if (this.isPathRelative) { 1505 | return new urllite.URL(this); 1506 | } 1507 | if (typeof other === 'string') { 1508 | other = urllite(other); 1509 | } 1510 | url = this.resolve(other); 1511 | if (url.origin && url.origin !== other.origin) { 1512 | throw new Error("Origins don't match (" + url.origin + " and " + other.origin + ")"); 1513 | } else if (!other.isAbsolute && !other.isAbsolutePathRelative) { 1514 | throw new Error("Other URL (<" + other + ">) is neither absolute nor absolute path relative."); 1515 | } 1516 | otherSegments = other.pathname.split('/').slice(1); 1517 | urlSegments = url.pathname.split('/').slice(1); 1518 | for (i = _i = 0, _len = urlSegments.length; _i < _len; i = ++_i) { 1519 | c = urlSegments[i]; 1520 | if (!(c === otherSegments[i] && (urlSegments.length > (_ref = i + 1) && _ref < otherSegments.length))) { 1521 | break; 1522 | } 1523 | } 1524 | newSegments = urlSegments.slice(i); 1525 | while (i < otherSegments.length - 1) { 1526 | if (otherSegments[i]) { 1527 | newSegments.unshift('..'); 1528 | } 1529 | i++; 1530 | } 1531 | if (newSegments.length === 1) { 1532 | newSegments = newSegments[0] === otherSegments[i] ? [''] : newSegments[0] === '' ? ['.'] : newSegments; 1533 | } 1534 | return urllite._createURL({ 1535 | pathname: newSegments.join('/'), 1536 | search: url.search, 1537 | hash: url.hash 1538 | }); 1539 | }; 1540 | 1541 | }).call(this); 1542 | 1543 | },{"../core":26,"./resolve":29}],29:[function(_dereq_,module,exports){ 1544 | (function() { 1545 | var URL, copyProps, oldParse, urllite, 1546 | __slice = [].slice; 1547 | 1548 | urllite = _dereq_('../core'); 1549 | 1550 | _dereq_('./normalize'); 1551 | 1552 | URL = urllite.URL; 1553 | 1554 | oldParse = URL.parse; 1555 | 1556 | copyProps = function() { 1557 | var prop, props, source, target, _i, _len; 1558 | target = arguments[0], source = arguments[1], props = 3 <= arguments.length ? __slice.call(arguments, 2) : []; 1559 | for (_i = 0, _len = props.length; _i < _len; _i++) { 1560 | prop = props[_i]; 1561 | target[prop] = source[prop]; 1562 | } 1563 | return target; 1564 | }; 1565 | 1566 | URL.parse = function(raw, opts) { 1567 | var base, url; 1568 | if (base = opts != null ? opts.base : void 0) { 1569 | delete opts.base; 1570 | } 1571 | url = oldParse(raw, opts); 1572 | if (base) { 1573 | return url.resolve(base); 1574 | } else { 1575 | return url; 1576 | } 1577 | }; 1578 | 1579 | URL.prototype.resolve = function(base) { 1580 | var p, prefix; 1581 | if (this.isAbsolute) { 1582 | return new urllite.URL(this); 1583 | } 1584 | if (typeof base === 'string') { 1585 | base = urllite(base); 1586 | } 1587 | p = {}; 1588 | if (this.isSchemeRelative) { 1589 | copyProps(p, this, 'username', 'password', 'host', 'hostname', 'port', 'pathname', 'search', 'hash'); 1590 | p.isSchemeRelative = !(p.protocol = base.protocol); 1591 | } else if (this.isAbsolutePathRelative || this.isPathRelative) { 1592 | copyProps(p, this, 'search', 'hash'); 1593 | copyProps(p, base, 'protocol', 'username', 'password', 'host', 'hostname', 'port'); 1594 | p.pathname = this.isPathRelative ? base.pathname.slice(0, -1) === '/' ? "" + base.pathname + "/" + this.pathname : (prefix = base.pathname.split('/').slice(0, -1).join('/'), prefix ? "" + prefix + "/" + this.pathname : this.pathname) : this.pathname; 1595 | } 1596 | return urllite._createURL(p).normalize(); 1597 | }; 1598 | 1599 | }).call(this); 1600 | 1601 | },{"../core":26,"./normalize":27}],30:[function(_dereq_,module,exports){ 1602 | (function() { 1603 | var URL, urllite; 1604 | 1605 | urllite = _dereq_('../core'); 1606 | 1607 | URL = urllite.URL; 1608 | 1609 | URL.prototype.toString = function() { 1610 | var authority, prefix, userinfo; 1611 | prefix = this.isSchemeRelative ? '//' : this.protocol === 'file:' ? "" + this.protocol + "///" : this.protocol ? "" + this.protocol + "//" : ''; 1612 | userinfo = this.password ? "" + this.username + ":" + this.password : this.username ? "" + this.username : ''; 1613 | authority = userinfo ? "" + userinfo + "@" + this.host : this.host ? "" + this.host : ''; 1614 | return "" + prefix + authority + this.pathname + this.search + this.hash; 1615 | }; 1616 | 1617 | }).call(this); 1618 | 1619 | },{"../core":26}],31:[function(_dereq_,module,exports){ 1620 | /*! 1621 | * EventEmitter v4.2.6 - git.io/ee 1622 | * Oliver Caldwell 1623 | * MIT license 1624 | * @preserve 1625 | */ 1626 | 1627 | (function () { 1628 | 'use strict'; 1629 | 1630 | /** 1631 | * Class for managing events. 1632 | * Can be extended to provide event functionality in other classes. 1633 | * 1634 | * @class EventEmitter Manages event registering and emitting. 1635 | */ 1636 | function EventEmitter() {} 1637 | 1638 | // Shortcuts to improve speed and size 1639 | var proto = EventEmitter.prototype; 1640 | var exports = this; 1641 | var originalGlobalValue = exports.EventEmitter; 1642 | 1643 | /** 1644 | * Finds the index of the listener for the event in it's storage array. 1645 | * 1646 | * @param {Function[]} listeners Array of listeners to search through. 1647 | * @param {Function} listener Method to look for. 1648 | * @return {Number} Index of the specified listener, -1 if not found 1649 | * @api private 1650 | */ 1651 | function indexOfListener(listeners, listener) { 1652 | var i = listeners.length; 1653 | while (i--) { 1654 | if (listeners[i].listener === listener) { 1655 | return i; 1656 | } 1657 | } 1658 | 1659 | return -1; 1660 | } 1661 | 1662 | /** 1663 | * Alias a method while keeping the context correct, to allow for overwriting of target method. 1664 | * 1665 | * @param {String} name The name of the target method. 1666 | * @return {Function} The aliased method 1667 | * @api private 1668 | */ 1669 | function alias(name) { 1670 | return function aliasClosure() { 1671 | return this[name].apply(this, arguments); 1672 | }; 1673 | } 1674 | 1675 | /** 1676 | * Returns the listener array for the specified event. 1677 | * Will initialise the event object and listener arrays if required. 1678 | * Will return an object if you use a regex search. The object contains keys for each matched event. So /ba[rz]/ might return an object containing bar and baz. But only if you have either defined them with defineEvent or added some listeners to them. 1679 | * Each property in the object response is an array of listener functions. 1680 | * 1681 | * @param {String|RegExp} evt Name of the event to return the listeners from. 1682 | * @return {Function[]|Object} All listener functions for the event. 1683 | */ 1684 | proto.getListeners = function getListeners(evt) { 1685 | var events = this._getEvents(); 1686 | var response; 1687 | var key; 1688 | 1689 | // Return a concatenated array of all matching events if 1690 | // the selector is a regular expression. 1691 | if (typeof evt === 'object') { 1692 | response = {}; 1693 | for (key in events) { 1694 | if (events.hasOwnProperty(key) && evt.test(key)) { 1695 | response[key] = events[key]; 1696 | } 1697 | } 1698 | } 1699 | else { 1700 | response = events[evt] || (events[evt] = []); 1701 | } 1702 | 1703 | return response; 1704 | }; 1705 | 1706 | /** 1707 | * Takes a list of listener objects and flattens it into a list of listener functions. 1708 | * 1709 | * @param {Object[]} listeners Raw listener objects. 1710 | * @return {Function[]} Just the listener functions. 1711 | */ 1712 | proto.flattenListeners = function flattenListeners(listeners) { 1713 | var flatListeners = []; 1714 | var i; 1715 | 1716 | for (i = 0; i < listeners.length; i += 1) { 1717 | flatListeners.push(listeners[i].listener); 1718 | } 1719 | 1720 | return flatListeners; 1721 | }; 1722 | 1723 | /** 1724 | * Fetches the requested listeners via getListeners but will always return the results inside an object. This is mainly for internal use but others may find it useful. 1725 | * 1726 | * @param {String|RegExp} evt Name of the event to return the listeners from. 1727 | * @return {Object} All listener functions for an event in an object. 1728 | */ 1729 | proto.getListenersAsObject = function getListenersAsObject(evt) { 1730 | var listeners = this.getListeners(evt); 1731 | var response; 1732 | 1733 | if (listeners instanceof Array) { 1734 | response = {}; 1735 | response[evt] = listeners; 1736 | } 1737 | 1738 | return response || listeners; 1739 | }; 1740 | 1741 | /** 1742 | * Adds a listener function to the specified event. 1743 | * The listener will not be added if it is a duplicate. 1744 | * If the listener returns true then it will be removed after it is called. 1745 | * If you pass a regular expression as the event name then the listener will be added to all events that match it. 1746 | * 1747 | * @param {String|RegExp} evt Name of the event to attach the listener to. 1748 | * @param {Function} listener Method to be called when the event is emitted. If the function returns true then it will be removed after calling. 1749 | * @return {Object} Current instance of EventEmitter for chaining. 1750 | */ 1751 | proto.addListener = function addListener(evt, listener) { 1752 | var listeners = this.getListenersAsObject(evt); 1753 | var listenerIsWrapped = typeof listener === 'object'; 1754 | var key; 1755 | 1756 | for (key in listeners) { 1757 | if (listeners.hasOwnProperty(key) && indexOfListener(listeners[key], listener) === -1) { 1758 | listeners[key].push(listenerIsWrapped ? listener : { 1759 | listener: listener, 1760 | once: false 1761 | }); 1762 | } 1763 | } 1764 | 1765 | return this; 1766 | }; 1767 | 1768 | /** 1769 | * Alias of addListener 1770 | */ 1771 | proto.on = alias('addListener'); 1772 | 1773 | /** 1774 | * Semi-alias of addListener. It will add a listener that will be 1775 | * automatically removed after it's first execution. 1776 | * 1777 | * @param {String|RegExp} evt Name of the event to attach the listener to. 1778 | * @param {Function} listener Method to be called when the event is emitted. If the function returns true then it will be removed after calling. 1779 | * @return {Object} Current instance of EventEmitter for chaining. 1780 | */ 1781 | proto.addOnceListener = function addOnceListener(evt, listener) { 1782 | return this.addListener(evt, { 1783 | listener: listener, 1784 | once: true 1785 | }); 1786 | }; 1787 | 1788 | /** 1789 | * Alias of addOnceListener. 1790 | */ 1791 | proto.once = alias('addOnceListener'); 1792 | 1793 | /** 1794 | * Defines an event name. This is required if you want to use a regex to add a listener to multiple events at once. If you don't do this then how do you expect it to know what event to add to? Should it just add to every possible match for a regex? No. That is scary and bad. 1795 | * You need to tell it what event names should be matched by a regex. 1796 | * 1797 | * @param {String} evt Name of the event to create. 1798 | * @return {Object} Current instance of EventEmitter for chaining. 1799 | */ 1800 | proto.defineEvent = function defineEvent(evt) { 1801 | this.getListeners(evt); 1802 | return this; 1803 | }; 1804 | 1805 | /** 1806 | * Uses defineEvent to define multiple events. 1807 | * 1808 | * @param {String[]} evts An array of event names to define. 1809 | * @return {Object} Current instance of EventEmitter for chaining. 1810 | */ 1811 | proto.defineEvents = function defineEvents(evts) { 1812 | for (var i = 0; i < evts.length; i += 1) { 1813 | this.defineEvent(evts[i]); 1814 | } 1815 | return this; 1816 | }; 1817 | 1818 | /** 1819 | * Removes a listener function from the specified event. 1820 | * When passed a regular expression as the event name, it will remove the listener from all events that match it. 1821 | * 1822 | * @param {String|RegExp} evt Name of the event to remove the listener from. 1823 | * @param {Function} listener Method to remove from the event. 1824 | * @return {Object} Current instance of EventEmitter for chaining. 1825 | */ 1826 | proto.removeListener = function removeListener(evt, listener) { 1827 | var listeners = this.getListenersAsObject(evt); 1828 | var index; 1829 | var key; 1830 | 1831 | for (key in listeners) { 1832 | if (listeners.hasOwnProperty(key)) { 1833 | index = indexOfListener(listeners[key], listener); 1834 | 1835 | if (index !== -1) { 1836 | listeners[key].splice(index, 1); 1837 | } 1838 | } 1839 | } 1840 | 1841 | return this; 1842 | }; 1843 | 1844 | /** 1845 | * Alias of removeListener 1846 | */ 1847 | proto.off = alias('removeListener'); 1848 | 1849 | /** 1850 | * Adds listeners in bulk using the manipulateListeners method. 1851 | * If you pass an object as the second argument you can add to multiple events at once. The object should contain key value pairs of events and listeners or listener arrays. You can also pass it an event name and an array of listeners to be added. 1852 | * You can also pass it a regular expression to add the array of listeners to all events that match it. 1853 | * Yeah, this function does quite a bit. That's probably a bad thing. 1854 | * 1855 | * @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to add to multiple events at once. 1856 | * @param {Function[]} [listeners] An optional array of listener functions to add. 1857 | * @return {Object} Current instance of EventEmitter for chaining. 1858 | */ 1859 | proto.addListeners = function addListeners(evt, listeners) { 1860 | // Pass through to manipulateListeners 1861 | return this.manipulateListeners(false, evt, listeners); 1862 | }; 1863 | 1864 | /** 1865 | * Removes listeners in bulk using the manipulateListeners method. 1866 | * If you pass an object as the second argument you can remove from multiple events at once. The object should contain key value pairs of events and listeners or listener arrays. 1867 | * You can also pass it an event name and an array of listeners to be removed. 1868 | * You can also pass it a regular expression to remove the listeners from all events that match it. 1869 | * 1870 | * @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to remove from multiple events at once. 1871 | * @param {Function[]} [listeners] An optional array of listener functions to remove. 1872 | * @return {Object} Current instance of EventEmitter for chaining. 1873 | */ 1874 | proto.removeListeners = function removeListeners(evt, listeners) { 1875 | // Pass through to manipulateListeners 1876 | return this.manipulateListeners(true, evt, listeners); 1877 | }; 1878 | 1879 | /** 1880 | * Edits listeners in bulk. The addListeners and removeListeners methods both use this to do their job. You should really use those instead, this is a little lower level. 1881 | * The first argument will determine if the listeners are removed (true) or added (false). 1882 | * If you pass an object as the second argument you can add/remove from multiple events at once. The object should contain key value pairs of events and listeners or listener arrays. 1883 | * You can also pass it an event name and an array of listeners to be added/removed. 1884 | * You can also pass it a regular expression to manipulate the listeners of all events that match it. 1885 | * 1886 | * @param {Boolean} remove True if you want to remove listeners, false if you want to add. 1887 | * @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to add/remove from multiple events at once. 1888 | * @param {Function[]} [listeners] An optional array of listener functions to add/remove. 1889 | * @return {Object} Current instance of EventEmitter for chaining. 1890 | */ 1891 | proto.manipulateListeners = function manipulateListeners(remove, evt, listeners) { 1892 | var i; 1893 | var value; 1894 | var single = remove ? this.removeListener : this.addListener; 1895 | var multiple = remove ? this.removeListeners : this.addListeners; 1896 | 1897 | // If evt is an object then pass each of it's properties to this method 1898 | if (typeof evt === 'object' && !(evt instanceof RegExp)) { 1899 | for (i in evt) { 1900 | if (evt.hasOwnProperty(i) && (value = evt[i])) { 1901 | // Pass the single listener straight through to the singular method 1902 | if (typeof value === 'function') { 1903 | single.call(this, i, value); 1904 | } 1905 | else { 1906 | // Otherwise pass back to the multiple function 1907 | multiple.call(this, i, value); 1908 | } 1909 | } 1910 | } 1911 | } 1912 | else { 1913 | // So evt must be a string 1914 | // And listeners must be an array of listeners 1915 | // Loop over it and pass each one to the multiple method 1916 | i = listeners.length; 1917 | while (i--) { 1918 | single.call(this, evt, listeners[i]); 1919 | } 1920 | } 1921 | 1922 | return this; 1923 | }; 1924 | 1925 | /** 1926 | * Removes all listeners from a specified event. 1927 | * If you do not specify an event then all listeners will be removed. 1928 | * That means every event will be emptied. 1929 | * You can also pass a regex to remove all events that match it. 1930 | * 1931 | * @param {String|RegExp} [evt] Optional name of the event to remove all listeners for. Will remove from every event if not passed. 1932 | * @return {Object} Current instance of EventEmitter for chaining. 1933 | */ 1934 | proto.removeEvent = function removeEvent(evt) { 1935 | var type = typeof evt; 1936 | var events = this._getEvents(); 1937 | var key; 1938 | 1939 | // Remove different things depending on the state of evt 1940 | if (type === 'string') { 1941 | // Remove all listeners for the specified event 1942 | delete events[evt]; 1943 | } 1944 | else if (type === 'object') { 1945 | // Remove all events matching the regex. 1946 | for (key in events) { 1947 | if (events.hasOwnProperty(key) && evt.test(key)) { 1948 | delete events[key]; 1949 | } 1950 | } 1951 | } 1952 | else { 1953 | // Remove all listeners in all events 1954 | delete this._events; 1955 | } 1956 | 1957 | return this; 1958 | }; 1959 | 1960 | /** 1961 | * Alias of removeEvent. 1962 | * 1963 | * Added to mirror the node API. 1964 | */ 1965 | proto.removeAllListeners = alias('removeEvent'); 1966 | 1967 | /** 1968 | * Emits an event of your choice. 1969 | * When emitted, every listener attached to that event will be executed. 1970 | * If you pass the optional argument array then those arguments will be passed to every listener upon execution. 1971 | * Because it uses `apply`, your array of arguments will be passed as if you wrote them out separately. 1972 | * So they will not arrive within the array on the other side, they will be separate. 1973 | * You can also pass a regular expression to emit to all events that match it. 1974 | * 1975 | * @param {String|RegExp} evt Name of the event to emit and execute listeners for. 1976 | * @param {Array} [args] Optional array of arguments to be passed to each listener. 1977 | * @return {Object} Current instance of EventEmitter for chaining. 1978 | */ 1979 | proto.emitEvent = function emitEvent(evt, args) { 1980 | var listeners = this.getListenersAsObject(evt); 1981 | var listener; 1982 | var i; 1983 | var key; 1984 | var response; 1985 | 1986 | for (key in listeners) { 1987 | if (listeners.hasOwnProperty(key)) { 1988 | i = listeners[key].length; 1989 | 1990 | while (i--) { 1991 | // If the listener returns true then it shall be removed from the event 1992 | // The function is executed either with a basic call or an apply if there is an args array 1993 | listener = listeners[key][i]; 1994 | 1995 | if (listener.once === true) { 1996 | this.removeListener(evt, listener.listener); 1997 | } 1998 | 1999 | response = listener.listener.apply(this, args || []); 2000 | 2001 | if (response === this._getOnceReturnValue()) { 2002 | this.removeListener(evt, listener.listener); 2003 | } 2004 | } 2005 | } 2006 | } 2007 | 2008 | return this; 2009 | }; 2010 | 2011 | /** 2012 | * Alias of emitEvent 2013 | */ 2014 | proto.trigger = alias('emitEvent'); 2015 | 2016 | /** 2017 | * Subtly different from emitEvent in that it will pass its arguments on to the listeners, as opposed to taking a single array of arguments to pass on. 2018 | * As with emitEvent, you can pass a regex in place of the event name to emit to all events that match it. 2019 | * 2020 | * @param {String|RegExp} evt Name of the event to emit and execute listeners for. 2021 | * @param {...*} Optional additional arguments to be passed to each listener. 2022 | * @return {Object} Current instance of EventEmitter for chaining. 2023 | */ 2024 | proto.emit = function emit(evt) { 2025 | var args = Array.prototype.slice.call(arguments, 1); 2026 | return this.emitEvent(evt, args); 2027 | }; 2028 | 2029 | /** 2030 | * Sets the current value to check against when executing listeners. If a 2031 | * listeners return value matches the one set here then it will be removed 2032 | * after execution. This value defaults to true. 2033 | * 2034 | * @param {*} value The new value to check for when executing listeners. 2035 | * @return {Object} Current instance of EventEmitter for chaining. 2036 | */ 2037 | proto.setOnceReturnValue = function setOnceReturnValue(value) { 2038 | this._onceReturnValue = value; 2039 | return this; 2040 | }; 2041 | 2042 | /** 2043 | * Fetches the current value to check against when executing listeners. If 2044 | * the listeners return value matches this one then it should be removed 2045 | * automatically. It will return true by default. 2046 | * 2047 | * @return {*|Boolean} The current value to check for or the default, true. 2048 | * @api private 2049 | */ 2050 | proto._getOnceReturnValue = function _getOnceReturnValue() { 2051 | if (this.hasOwnProperty('_onceReturnValue')) { 2052 | return this._onceReturnValue; 2053 | } 2054 | else { 2055 | return true; 2056 | } 2057 | }; 2058 | 2059 | /** 2060 | * Fetches the events object and creates one if required. 2061 | * 2062 | * @return {Object} The events storage object. 2063 | * @api private 2064 | */ 2065 | proto._getEvents = function _getEvents() { 2066 | return this._events || (this._events = {}); 2067 | }; 2068 | 2069 | /** 2070 | * Reverts the global {@link EventEmitter} to its previous value and returns a reference to this version. 2071 | * 2072 | * @return {Function} Non conflicting EventEmitter class. 2073 | */ 2074 | EventEmitter.noConflict = function noConflict() { 2075 | exports.EventEmitter = originalGlobalValue; 2076 | return EventEmitter; 2077 | }; 2078 | 2079 | // Expose the class either via AMD, CommonJS or the global object 2080 | if (typeof define === 'function' && define.amd) { 2081 | define(function () { 2082 | return EventEmitter; 2083 | }); 2084 | } 2085 | else if (typeof module === 'object' && module.exports){ 2086 | module.exports = EventEmitter; 2087 | } 2088 | else { 2089 | this.EventEmitter = EventEmitter; 2090 | } 2091 | }.call(this)); 2092 | 2093 | },{}],32:[function(_dereq_,module,exports){ 2094 | module.exports = extend 2095 | 2096 | function extend() { 2097 | var target = {} 2098 | 2099 | for (var i = 0; i < arguments.length; i++) { 2100 | var source = arguments[i] 2101 | 2102 | for (var key in source) { 2103 | if (source.hasOwnProperty(key)) { 2104 | target[key] = source[key] 2105 | } 2106 | } 2107 | } 2108 | 2109 | return target 2110 | } 2111 | 2112 | },{}],33:[function(_dereq_,module,exports){ 2113 | module.exports = extend 2114 | 2115 | function extend(target) { 2116 | for (var i = 1; i < arguments.length; i++) { 2117 | var source = arguments[i] 2118 | 2119 | for (var key in source) { 2120 | if (source.hasOwnProperty(key)) { 2121 | target[key] = source[key] 2122 | } 2123 | } 2124 | } 2125 | 2126 | return target 2127 | } 2128 | 2129 | },{}]},{},[10]) 2130 | (10) 2131 | }); --------------------------------------------------------------------------------