UNPKG

25.9 kBMarkdownView Raw
1![Logo](https://github.com/flatiron/director/raw/master/img/director.png)
2
3# Synopsis
4
5Director is a router. Routing is the process of determining what code to run
6when a URL is requested.
7
8# Motivation
9
10A routing library that works in both the browser and node.js environments with
11as few differences as possible. Simplifies the development of Single Page Apps
12and Node.js applications. Dependency free (doesn't require jQuery or Express,
13etc).
14
15# Status
16[![Build Status](https://secure.travis-ci.org/flatiron/director.png?branch=master)](http://travis-ci.org/flatiron/director)
17
18# Features
19
20* [Client-Side Routing](#client-side-routing)
21* [Server-Side HTTP Routing](#server-side-http-routing)
22* [Server-Side CLI Routing](#server-side-cli-routing)
23
24# Usage
25
26* [API Documentation](#api-documentation)
27* [Frequently Asked Questions](#faq)
28
29## Building client-side script
30
31Run the provided CLI script.
32
33```bash
34./bin/build
35```
36
37## Client-side Routing
38
39It simply watches the hash of the URL to determine what to do, for example:
40
41```
42http://foo.com/#/bar
43```
44
45Client-side routing (aka hash-routing) allows you to specify some information
46about the state of the application using the URL. So that when the user visits
47a specific URL, the application can be transformed accordingly.
48
49![Hash route](https://github.com/flatiron/director/raw/master/img/hashRoute.png)
50
51Here is a simple example:
52
53```html
54<!DOCTYPE html>
55<html>
56 <head>
57 <meta charset="utf-8">
58 <title>A Gentle Introduction</title>
59
60 <script
61 src="https://rawgit.com/flatiron/director/master/build/director.min.js">
62 </script>
63
64 <script>
65 var author = function () { console.log("author"); };
66 var books = function () { console.log("books"); };
67 var viewBook = function (bookId) {
68 console.log("viewBook: bookId is populated: " + bookId);
69 };
70
71 var routes = {
72 '/author': author,
73 '/books': [books, function() {
74 console.log("An inline route handler.");
75 }],
76 '/books/view/:bookId': viewBook
77 };
78
79 var router = Router(routes);
80
81 router.init();
82 </script>
83 </head>
84
85 <body>
86 <ul>
87 <li><a href="#/author">#/author</a></li>
88 <li><a href="#/books">#/books</a></li>
89 <li><a href="#/books/view/1">#/books/view/1</a></li>
90 </ul>
91 </body>
92</html>
93```
94
95Director works great with your favorite DOM library, such as jQuery.
96
97```html
98<!DOCTYPE html>
99<html>
100 <head>
101 <meta charset="utf-8">
102 <title>A Gentle Introduction 2</title>
103
104 <script
105 src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js">
106 </script>
107
108 <script
109 src="https://rawgit.com/flatiron/director/master/build/director.min.js">
110 </script>
111
112 <script>
113 $('document').ready(function() {
114 //
115 // create some functions to be executed when
116 // the correct route is issued by the user.
117 //
118 var showAuthorInfo = function () { console.log("showAuthorInfo"); };
119 var listBooks = function () { console.log("listBooks"); };
120
121 var allroutes = function() {
122 var route = window.location.hash.slice(2);
123 var sections = $('section');
124 var section;
125
126 section = sections.filter('[data-route=' + route + ']');
127
128 if (section.length) {
129 sections.hide(250);
130 section.show(250);
131 }
132 };
133
134 //
135 // define the routing table.
136 //
137 var routes = {
138 '/author': showAuthorInfo,
139 '/books': listBooks
140 };
141
142 //
143 // instantiate the router.
144 //
145 var router = Router(routes);
146
147 //
148 // a global configuration setting.
149 //
150 router.configure({
151 on: allroutes
152 });
153
154 router.init();
155 });
156 </script>
157 </head>
158
159 <body>
160 <section data-route="author">Author Name</section>
161 <section data-route="books">Book1, Book2, Book3</section>
162 <ul>
163 <li><a href="#/author">#/author</a></li>
164 <li><a href="#/books">#/books</a></li>
165 </ul>
166 </body>
167</html>
168```
169
170You can find a browser-specific build of `director` [here][1] which has all of
171the server code stripped away.
172
173## Server-Side HTTP Routing
174
175Director handles routing for HTTP requests similar to `journey` or `express`:
176
177```js
178 //
179 // require the native http module, as well as director.
180 //
181 var http = require('http'),
182 director = require('director');
183
184 //
185 // create some logic to be routed to.
186 //
187 function helloWorld() {
188 this.res.writeHead(200, { 'Content-Type': 'text/plain' })
189 this.res.end('hello world');
190 }
191
192 //
193 // define a routing table.
194 //
195 var router = new director.http.Router({
196 '/hello': {
197 get: helloWorld
198 }
199 });
200
201 //
202 // setup a server and when there is a request, dispatch the
203 // route that was requested in the request object.
204 //
205 var server = http.createServer(function (req, res) {
206 router.dispatch(req, res, function (err) {
207 if (err) {
208 res.writeHead(404);
209 res.end();
210 }
211 });
212 });
213
214 //
215 // You can also do ad-hoc routing, similar to `journey` or `express`.
216 // This can be done with a string or a regexp.
217 //
218 router.get('/bonjour', helloWorld);
219 router.get(/hola/, helloWorld);
220
221 //
222 // set the server to listen on port `8080`.
223 //
224 server.listen(8080);
225```
226
227### See Also:
228
229 - Auto-generated Node.js API Clients for routers using
230 [Director-Reflector](http://github.com/flatiron/director-reflector)
231 - RESTful Resource routing using [restful](http://github.com/flatiron/restful)
232 - HTML / Plain Text views of routers using
233 [Director-Explorer](http://github.com/flatiron/director-explorer)
234
235## Server-Side CLI Routing
236
237Director supports Command Line Interface routing. Routes for cli options are
238based on command line input (i.e. `process.argv`) instead of a URL.
239
240``` js
241 var director = require('director');
242
243 var router = new director.cli.Router();
244
245 router.on('create', function () {
246 console.log('create something');
247 });
248
249 router.on(/destroy/, function () {
250 console.log('destroy something');
251 });
252
253 // You will need to dispatch the cli arguments yourself
254 router.dispatch('on', process.argv.slice(2).join(' '));
255```
256
257Using the cli router, you can dispatch commands by passing them as a string.
258For example, if this example is in a file called `foo.js`:
259
260```bash
261$ node foo.js create
262create something
263$ node foo.js destroy
264destroy something
265```
266
267# API Documentation
268
269* [Constructor](#constructor)
270* [Routing Table](#routing-table)
271* [Adhoc Routing](#adhoc-routing)
272* [Scoped Routing](#scoped-routing)
273* [Routing Events](#routing-events)
274* [Configuration](#configuration)
275* [URL Matching](#url-matching)
276* [URL Parameters](#url-parameters)
277* [Route Recursion](#route-recursion)
278* [Async Routing](#async-routing)
279* [Resources](#resources)
280* [History API](#history-api)
281* [Instance Methods](#instance-methods)
282* [Attach Properties to `this`](#attach-to-this)
283* [HTTP Streaming and Body Parsing](#http-streaming-body-parsing)
284
285## Constructor
286
287``` js
288 var router = Router(routes);
289```
290
291## Routing Table
292
293An object literal that contains nested route definitions. A potentially nested
294set of key/value pairs. The keys in the object literal represent each potential
295part of the URL. The values in the object literal contain references to the
296functions that should be associated with them. *bark* and *meow* are two
297functions that you have defined in your code.
298
299``` js
300 //
301 // Assign routes to an object literal.
302 //
303 var routes = {
304 //
305 // a route which assigns the function `bark`.
306 //
307 '/dog': bark,
308 //
309 // a route which assigns the functions `meow` and `scratch`.
310 //
311 '/cat': [meow, scratch]
312 };
313
314 //
315 // Instantiate the router.
316 //
317 var router = Router(routes);
318```
319
320## Adhoc Routing
321
322When developing large client-side or server-side applications it is not always
323possible to define routes in one location. Usually individual decoupled
324components register their own routes with the application router. We refer to
325this as _Adhoc Routing._ Lets take a look at the API `director` exposes for
326adhoc routing:
327
328**Client-side Routing**
329
330``` js
331 var router = new Router().init();
332
333 router.on('/some/resource', function () {
334 //
335 // Do something on `/#/some/resource`
336 //
337 });
338```
339
340**HTTP Routing**
341
342``` js
343 var router = new director.http.Router();
344
345 router.get(/\/some\/resource/, function () {
346 //
347 // Do something on an GET to `/some/resource`
348 //
349 });
350```
351
352## Scoped Routing
353
354In large web appliations, both [Client-side](#client-side) and
355[Server-side](#http-routing), routes are often scoped within a few individual
356resources. Director exposes a simple way to do this for [Adhoc
357Routing](#adhoc-routing) scenarios:
358
359``` js
360 var router = new director.http.Router();
361
362 //
363 // Create routes inside the `/users` scope.
364 //
365 router.path(/\/users\/(\w+)/, function () {
366 //
367 // The `this` context of the function passed to `.path()`
368 // is the Router itself.
369 //
370
371 this.post(function (id) {
372 //
373 // Create the user with the specified `id`.
374 //
375 });
376
377 this.get(function (id) {
378 //
379 // Retreive the user with the specified `id`.
380 //
381 });
382
383 this.get(/\/friends/, function (id) {
384 //
385 // Get the friends for the user with the specified `id`.
386 //
387 });
388 });
389```
390
391## Routing Events
392
393In `director`, a "routing event" is a named property in the
394[Routing Table](#routing-table) which can be assigned to a function or an Array
395of functions to be called when a route is matched in a call to
396`router.dispatch()`.
397
398* **on:** A function or Array of functions to execute when the route is matched.
399* **before:** A function or Array of functions to execute before calling the
400 `on` method(s).
401
402**Client-side only**
403
404* **after:** A function or Array of functions to execute when leaving a
405 particular route.
406* **once:** A function or Array of functions to execute only once for a
407 particular route.
408
409## Configuration
410
411Given the flexible nature of `director` there are several options available for
412both the [Client-side](#client-side) and [Server-side](#http-routing). These
413options can be set using the `.configure()` method:
414
415``` js
416 var router = new director.Router(routes).configure(options);
417```
418
419The `options` are:
420
421* **recurse:** Controls [route recursion](#route-recursion). Use `forward`,
422 `backward`, or `false`. Default is `false` Client-side, and `backward`
423 Server-side.
424* **strict:** If set to `false`, then trailing slashes (or other delimiters)
425 are allowed in routes. Default is `true`.
426* **async:** Controls [async routing](#async-routing). Use `true` or `false`.
427 Default is `false`.
428* **delimiter:** Character separator between route fragments. Default is `/`.
429* **notfound:** A function to call if no route is found on a call to
430 `router.dispatch()`.
431* **on:** A function (or list of functions) to call on every call to
432 `router.dispatch()` when a route is found.
433* **before:** A function (or list of functions) to call before every call to
434 `router.dispatch()` when a route is found.
435
436**Client-side only**
437
438* **resource:** An object to which string-based routes will be bound. This can
439 be especially useful for late-binding to route functions (such as async
440 client-side requires).
441* **after:** A function (or list of functions) to call when a given route is no
442 longer the active route.
443* **html5history:** If set to `true` and client supports `pushState()`, then
444 uses HTML5 History API instead of hash fragments. See
445 [History API](#history-api) for more information.
446* **run_handler_in_init:** If `html5history` is enabled, the route handler by
447 default is executed upon `Router.init()` since with real URIs the router can
448 not know if it should call a route handler or not. Setting this to `false`
449 disables the route handler initial execution.
450* **convert_hash_in_init:** If `html5history` is enabled, the window.location hash by default is converted to a route upon `Router.init()` since with canonical URIs the router can not know if it should convert the hash to a route or not. Setting this to `false` disables the hash conversion on router initialisation.
451
452## URL Matching
453
454``` js
455 var router = Router({
456 //
457 // given the route '/dog/yella'.
458 //
459 '/dog': {
460 '/:color': {
461 //
462 // this function will return the value 'yella'.
463 //
464 on: function (color) { console.log(color) }
465 }
466 }
467 });
468```
469
470Routes can sometimes become very complex, `simple/:tokens` don't always
471suffice. Director supports regular expressions inside the route names. The
472values captured from the regular expressions are passed to your listener
473function.
474
475``` js
476 var router = Router({
477 //
478 // given the route '/hello/world'.
479 //
480 '/hello': {
481 '/(\\w+)': {
482 //
483 // this function will return the value 'world'.
484 //
485 on: function (who) { console.log(who) }
486 }
487 }
488 });
489```
490
491``` js
492 var router = Router({
493 //
494 // given the route '/hello/world/johny/appleseed'.
495 //
496 '/hello': {
497 '/world/?([^\/]*)\/([^\/]*)/?': function (a, b) {
498 console.log(a, b);
499 }
500 }
501 });
502```
503
504## URL Parameters
505
506When you are using the same route fragments it is more descriptive to define
507these fragments by name and then use them in your
508[Routing Table](#routing-table) or [Adhoc Routes](#adhoc-routing). Consider a
509simple example where a `userId` is used repeatedly.
510
511``` js
512 //
513 // Create a router. This could also be director.cli.Router() or
514 // director.http.Router().
515 //
516 var router = new director.Router();
517
518 //
519 // A route could be defined using the `userId` explicitly.
520 //
521 router.on(/([\w-_]+)/, function (userId) { });
522
523 //
524 // Define a shorthand for this fragment called `userId`.
525 //
526 router.param('userId', /([\\w\\-]+)/);
527
528 //
529 // Now multiple routes can be defined with the same
530 // regular expression.
531 //
532 router.on('/anything/:userId', function (userId) { });
533 router.on('/something-else/:userId', function (userId) { });
534```
535
536## Route Recursion
537
538Can be assigned the value of `forward` or `backward`. The recurse option will
539determine the order in which to fire the listeners that are associated with
540your routes. If this option is NOT specified or set to null, then only the
541listeners associated with an exact match will be fired.
542
543### No recursion, with the URL /dog/angry
544
545``` js
546 var routes = {
547 '/dog': {
548 '/angry': {
549 //
550 // Only this method will be fired.
551 //
552 on: growl
553 },
554 on: bark
555 }
556 };
557
558 var router = Router(routes);
559```
560
561### Recursion set to `backward`, with the URL /dog/angry
562
563``` js
564 var routes = {
565 '/dog': {
566 '/angry': {
567 //
568 // This method will be fired first.
569 //
570 on: growl
571 },
572 //
573 // This method will be fired second.
574 //
575 on: bark
576 }
577 };
578
579 var router = Router(routes).configure({ recurse: 'backward' });
580```
581
582### Recursion set to `forward`, with the URL /dog/angry
583
584``` js
585 var routes = {
586 '/dog': {
587 '/angry': {
588 //
589 // This method will be fired second.
590 //
591 on: growl
592 },
593 //
594 // This method will be fired first.
595 //
596 on: bark
597 }
598 };
599
600 var router = Router(routes).configure({ recurse: 'forward' });
601```
602
603### Breaking out of recursion, with the URL /dog/angry
604
605``` js
606 var routes = {
607 '/dog': {
608 '/angry': {
609 //
610 // This method will be fired first.
611 //
612 on: function() { return false; }
613 },
614 //
615 // This method will not be fired.
616 //
617 on: bark
618 }
619 };
620
621 //
622 // This feature works in reverse with recursion set to true.
623 //
624 var router = Router(routes).configure({ recurse: 'backward' });
625```
626
627## Async Routing
628
629Before diving into how Director exposes async routing, you should understand
630[Route Recursion](#route-recursion). At it's core route recursion is about
631evaluating a series of functions gathered when traversing the [Routing
632Table](#routing-table).
633
634Normally this series of functions is evaluated synchronously. In async routing,
635these functions are evaluated asynchronously. Async routing can be extremely
636useful both on the client-side and the server-side:
637
638* **Client-side:** To ensure an animation or other async operations (such as
639 HTTP requests for authentication) have completed before continuing evaluation
640 of a route.
641* **Server-side:** To ensure arbitrary async operations (such as performing
642 authentication) have completed before continuing the evaluation of a route.
643
644The method signatures for route functions in synchronous and asynchronous
645evaluation are different: async route functions take an additional `next()`
646callback.
647
648### Synchronous route functions
649
650``` js
651 var router = new director.Router();
652
653 router.on('/:foo/:bar/:bazz', function (foo, bar, bazz) {
654 //
655 // Do something asynchronous with `foo`, `bar`, and `bazz`.
656 //
657 });
658```
659
660### Asynchronous route functions
661
662``` js
663 var router = new director.http.Router().configure({ async: true });
664
665 router.on('/:foo/:bar/:bazz', function (foo, bar, bazz, next) {
666 //
667 // Go do something async, and determine that routing should stop
668 //
669 next(false);
670 });
671```
672
673## Resources
674
675**Available on the Client-side only.** An object literal containing functions.
676If a host object is specified, your route definitions can provide string
677literals that represent the function names inside the host object. A host
678object can provide the means for better encapsulation and design.
679
680``` js
681
682 var router = Router({
683
684 '/hello': {
685 '/usa': 'americas',
686 '/china': 'asia'
687 }
688
689 }).configure({ resource: container }).init();
690
691 var container = {
692 americas: function() { return true; },
693 china: function() { return true; }
694 };
695
696```
697
698## History API
699
700**Available on the Client-side only.** Director supports using HTML5 History
701API instead of hash fragments for navigation. To use the API, pass
702`{html5history: true}` to `configure()`. Use of the API is enabled only if the
703client supports `pushState()`.
704
705Using the API gives you cleaner URIs but they come with a cost. Unlike with
706hash fragments your route URIs must exist. When the client enters a page, say
707http://foo.com/bar/baz, the web server must respond with something meaningful.
708Usually this means that your web server checks the URI points to something
709that, in a sense, exists, and then serves the client the JavaScript
710application.
711
712If you're after a single-page application you can not use plain old `<a
713href="/bar/baz">` tags for navigation anymore. When such link is clicked, web
714browsers try to ask for the resource from server which is not of course desired
715for a single-page application. Instead you need to use e.g. click handlers and
716call the `setRoute()` method yourself.
717
718## Attach Properties To `this`
719
720**Available in the http router only.** Generally, the `this` object bound to
721route handlers, will contain the request in `this.req` and the response in
722`this.res`. One may attach additional properties to `this` with the
723`router.attach` method:
724
725```js
726 var director = require('director');
727
728 var router = new director.http.Router().configure(options);
729
730 //
731 // Attach properties to `this`
732 //
733 router.attach(function () {
734 this.data = [1,2,3];
735 });
736
737 //
738 // Access properties attached to `this` in your routes!
739 //
740 router.get('/hello', function () {
741 this.res.writeHead(200, { 'content-type': 'text/plain' });
742
743 //
744 // Response will be `[1,2,3]`!
745 //
746 this.res.end(this.data);
747 });
748```
749
750This API may be used to attach convenience methods to the `this` context of
751route handlers.
752
753## HTTP Streaming and Body Parsing
754
755When you are performing HTTP routing there are two common scenarios:
756
757* Buffer the request body and parse it according to the `Content-Type` header
758 (usually `application/json` or `application/x-www-form-urlencoded`).
759* Stream the request body by manually calling `.pipe` or listening to the
760 `data` and `end` events.
761
762By default `director.http.Router()` will attempt to parse either the `.chunks`
763or `.body` properties set on the request parameter passed to
764`router.dispatch(request, response, callback)`. The router instance will also
765wait for the `end` event before firing any routes.
766
767**Default Behavior**
768
769``` js
770 var director = require('director');
771
772 var router = new director.http.Router();
773
774 router.get('/', function () {
775 //
776 // This will not work, because all of the data
777 // events and the end event have already fired.
778 //
779 this.req.on('data', function (chunk) {
780 console.log(chunk)
781 });
782 });
783```
784
785In [flatiron][2], `director` is used in conjunction with [union][3] which uses
786a `BufferedStream` proxy to the raw `http.Request` instance. [union][3] will
787set the `req.chunks` property for you and director will automatically parse the
788body. If you wish to perform this buffering yourself directly with `director`
789you can use a simple request handler in your http server:
790
791``` js
792 var http = require('http'),
793 director = require('director');
794
795 var router = new director.http.Router();
796
797 var server = http.createServer(function (req, res) {
798 req.chunks = [];
799 req.on('data', function (chunk) {
800 req.chunks.push(chunk.toString());
801 });
802
803 router.dispatch(req, res, function (err) {
804 if (err) {
805 res.writeHead(404);
806 res.end();
807 }
808
809 console.log('Served ' + req.url);
810 });
811 });
812
813 router.post('/', function () {
814 this.res.writeHead(200, { 'Content-Type': 'application/json' })
815 this.res.end(JSON.stringify(this.req.body));
816 });
817```
818
819**Streaming Support**
820
821If you wish to get access to the request stream before the `end` event is
822fired, you can pass the `{ stream: true }` options to the route.
823
824``` js
825 var director = require('director');
826
827 var router = new director.http.Router();
828
829 router.get('/', { stream: true }, function () {
830 //
831 // This will work because the route handler is invoked
832 // immediately without waiting for the `end` event.
833 //
834 this.req.on('data', function (chunk) {
835 console.log(chunk);
836 });
837 });
838```
839
840## Instance methods
841
842### configure(options)
843
844* `options` {Object}: Options to configure this instance with.
845
846Configures the Router instance with the specified `options`. See
847[Configuration](#configuration) for more documentation.
848
849### param(token, matcher)
850
851* token {string}: Named parameter token to set to the specified `matcher`
852* matcher {string|Regexp}: Matcher for the specified `token`.
853
854Adds a route fragment for the given string `token` to the specified regex
855`matcher` to this Router instance. See [URL Parameters](#url-parameters) for more
856documentation.
857
858### on(method, path, route)
859
860* `method` {string}: Method to insert within the Routing Table (e.g. `on`,
861 `get`, etc.).
862* `path` {string}: Path within the Routing Table to set the `route` to.
863* `route` {function|Array}: Route handler to invoke for the `method` and `path`.
864
865Adds the `route` handler for the specified `method` and `path` within the
866[Routing Table](#routing-table).
867
868### path(path, routesFn)
869
870* `path` {string|Regexp}: Scope within the Routing Table to invoke the
871 `routesFn` within.
872* `routesFn` {function}: Adhoc Routing function with calls to `this.on()`,
873 `this.get()` etc.
874
875Invokes the `routesFn` within the scope of the specified `path` for this Router
876instance.
877
878### dispatch(method, path[, callback])
879
880* method {string}: Method to invoke handlers for within the Routing Table
881* path {string}: Path within the Routing Table to match
882* callback {function}: Invoked once all route handlers have been called.
883
884Dispatches the route handlers matched within the [Routing Table](#routing-table)
885for this instance for the specified `method` and `path`.
886
887### mount(routes, path)
888
889* routes {object}: Partial routing table to insert into this instance.
890* path {string|Regexp}: Path within the Routing Table to insert the `routes`
891 into.
892
893Inserts the partial [Routing Table](#routing-table), `routes`, into the Routing
894Table for this Router instance at the specified `path`.
895
896## Instance methods (Client-side only)
897
898### init([redirect])
899
900* `redirect` {String}: This value will be used if '/#/' is not found in the
901 URL. (e.g., init('/') will resolve to '/#/', init('foo') will resolve to
902 '/#foo').
903
904Initialize the router, start listening for changes to the URL.
905
906### getRoute([index])
907
908* `index` {Number}: The hash value is divided by forward slashes, each section
909 then has an index, if this is provided, only that section of the route will
910 be returned.
911
912Returns the entire route or just a section of it.
913
914### setRoute(route)
915
916* `route` {String}: Supply a route value, such as `home/stats`.
917
918Set the current route.
919
920### setRoute(start, length)
921
922* `start` {Number} - The position at which to start removing items.
923* `length` {Number} - The number of items to remove from the route.
924
925Remove a segment from the current route.
926
927### setRoute(index, value)
928
929* `index` {Number} - The hash value is divided by forward slashes, each section
930 then has an index.
931* `value` {String} - The new value to assign the the position indicated by the
932 first parameter.
933
934Set a segment of the current route.
935
936# Frequently Asked Questions
937
938## What About SEO?
939
940Is using a Client-side router a problem for SEO? Yes. If advertising is a
941requirement, you are probably building a "Web Page" and not a "Web
942Application". Director on the client is meant for script-heavy Web
943Applications.
944
945##### LICENSE: MIT
946##### Author: [Charlie Robbins](https://github.com/indexzero)
947##### Contributors: [Paolo Fragomeni](https://github.com/hij1nx)
948
949[0]: http://github.com/flatiron/director
950[1]: https://github.com/flatiron/director/blob/master/build/director.min.js
951[2]: http://github.com/flatiron/flatiron
952[3]: http://github.com/flatiron/union