diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..e69de29b diff --git a/History.md b/History.md new file mode 100644 index 00000000..496de643 --- /dev/null +++ b/History.md @@ -0,0 +1,130 @@ +## vNEXT + +## v0.7.2 +* Integrate iron-layout for new Blaze.View work +* Only use one main computation in _run instead of one for each thing +* Fix clearUnusedRegion bug +* Fix ie9 omits leading slash in pathname on click handler +* Dont call unnecessary stop() in client router when browsing away +* Examples corrections + +Contributors: + Chris Mather + Dan Dascalescu + David Gomes + Tom Coleman + +## v0.7.1 +* Fix dataNotFoundHook +* Bump blaze-layout to 0.2.4 + * Better error message for {{yield}} vs {{> yield}} + * Make parent data context available inside Layout when using Layout manually +* Remove Handlebars symbol from helpers.js +* Fix client helpers processArgs bug +* Don't call the data() function until controller is ready + +## v0.7.0 +* Blaze rendering with the [blaze-layout](https://github.com/eventedmind/blaze-layout) package. + * Layouts are only taken off the DOM (re-rendered) if the layout changes. + * Templates are only taken off the DOM (re-rendered) if the template changes. + * Data contexts change independently from rendering. + * {{yield}} is now {{> yield}} and {{yield 'footer'}} is now {{> yield region='footer'}} + * {{#contentFor region='footer'}}footer content goes here{{/contentFor}} is supported again! + * Router supports a uiManager api that can be used to plug in other ui managers (in addition to blaze-layout) +* RouteController API cleanup + * Hook name changes (legacy supported until 1.0) + * before -> onBeforeAction + * after -> onAfterAction + * load -> onRun + * unload -> onStop + * No more getData and setData methods on RouteController instances. Just use `this.data()` to call the controller's wrapped data function. + * run method changes + * Changed run to _run to indicate privacy + * A RouteController is in a running state or a stopped state. You cannot run a controller that is already running. + * You cannot call `stop()` inside of a run. Use `pause()` for hooks now instead. see below. + * Order of operations: + 1. Clear the waitlist + 2. Set the layout + 3. Run the onRun hooks in a computation + 4. Run the waitOn function in a computation, populating the waitlist + 5. Set the global data context using the controller's wrapped data function + 6. Run the action in a computation in this order: onBeforeAction, action, onAfterAction + * Hook api changes + * You can no longer call `this.stop()` in a hook. Use `pause()` instead which is the first parameter passed to the hook function. This stops downstream hooks from running. For example the loading hook uses pause() to stop the action function from rendering the main template. + * No hooks are included in your controllers by default. If you want to add them you can do it like this: + * `Router.onBeforeAction('loading')` + * `Router.onBeforeAction('dataNotFound')` + * Package authors can include their own hooks in the lookup chain by adding them to the `Router.hooks` namespace. Then users can add them by name like this: `Router.onBeforeAction('customhook');` + * See lib/client/hooks.js for example. + * Hooks are now called in a different order by popular demand: + 1. controller options + 2. controller prototype + 3. controller object + 4. route option hooks + 5. router global + +* Helpers cleanup + * `{{link}}` helper is no longer included by default. These types of helpers can be implemented in separate packages. + * `{{renderRouter}}` is gone for now. + * `{{pathFor}}` and `{{urlFor}}` still work with some api changes: + * {{pathFor 'routeName' params=this query="key=value&key2=value2" hash="somehash" anotherparam="anothervalue"}} + * same for {{urlFor}} + +* IronLocation changes + * The router now sets up the link handler for much more consistency between `Router.go` and clicks on links. + * The location URL changes in between stopping the old route and starting the new one, so `onStop` and `onRun` behave as you'd expect. + * `location` is now an option to configure so you can use a custom location manager (apart from IronLocation). + +## v0.6.2 +* Bug fix: couldn't go back after page reload. Thanks @apendua! +* Added ability to customize IronLocation link selector. Thanks @nathan-muir +* Fixed a problem with child hooks running multiple times. Thanks @jagi! +* Fixed a problem with stopping the process when redirecting +* Fixed issues with optional paths, thanks @mitar +* Fixed problem on Android 2.3 + + + +## v0.6.1 +* Bug fix: notFound template rendered with layout +* Bug fix: loading and notFound render yeilds again +* Bug fix: IE8 issue with 'class' property name on link handlebars helper +* Bug fix: Global hook regression +* Readme fixes + +## v0.6.0 +* **WARNING:** Breaking Changes + * The `layout` option is now called `layoutTemplate` + * The `renderTemplates` option is now called `yieldTemplates` + * RouteController `onBefore..` and `onAfter..` methods removed (now just use + `before` and `after`) + * `Router.current()` now returns a `RouteController` instance + * data option now applies only to the Route or RouteController, not to the render method + * pathFor and urlFor semantics have changed slightly (hash and query params can now be the key value pairs of the Handlebars expression) + 1. `{{pathFor contextObject queryKey=queryValue hash=anchorTag}}` + or + 2. ``` + {{#with contextObject}} + {{pathFor queryKey=queryValue hash=anchorTag}} + {{/with}} + ``` + +* Route and RouteController level layouts +* Support for url hash fragments +* Better support for query string parameters +* PageManager class for handling layout and template rendering and storing a global data context + * Layouts and templates now only re-render if the actual template has changed (allows for maintaining a layout/template across routes with no flicker) + * Data context is set/get globally on/from the Router + * See the lib/client/page_controller.js file for details +* No more silly RouteContext; all this stuff is in the RouteController instance +* Partial support for IE8-9. Pages make a server request if pushState is not supported by the browser. This is a performance penalty, but it works +* Cleaned up API signatures and passing of options from Router->Route->RouteController +* Fix onclick handler and moved into lib/client/location.js +* Client and Server Router now inherit from IronRouter +* Client and Server RouteController now inherit from IronRouterController +* Removed unnecessary global symbol exports (still accessible through Package['iron-router'] namespace) +* Global `notFoundTemplate` will render if a route is not found. +* Added `load` hook which fires exactly once per route load (and respects hot-code-reload!) +* Added `Router.before()` and friends which let you add global hooks with a bit more subtlety. + +## v0.5.4 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..f9b5f928 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 Chris Mather + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..3eb12117 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +Iron.Router +============================================================================== + +## License +MIT diff --git a/examples/basic/.meteor/.gitignore b/examples/basic/.meteor/.gitignore new file mode 100644 index 00000000..40830374 --- /dev/null +++ b/examples/basic/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/examples/basic/.meteor/identifier b/examples/basic/.meteor/identifier new file mode 100644 index 00000000..ab1e7086 --- /dev/null +++ b/examples/basic/.meteor/identifier @@ -0,0 +1 @@ +6l3fq3iso6eb1u25omo \ No newline at end of file diff --git a/examples/basic/.meteor/packages b/examples/basic/.meteor/packages new file mode 100644 index 00000000..35fa0b57 --- /dev/null +++ b/examples/basic/.meteor/packages @@ -0,0 +1,10 @@ +# Meteor packages used by this project, one per line. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +standard-app-packages +autopublish +insecure +iron:router + diff --git a/examples/basic/.meteor/release b/examples/basic/.meteor/release new file mode 100644 index 00000000..db5f2c74 --- /dev/null +++ b/examples/basic/.meteor/release @@ -0,0 +1 @@ +0.8.1.3 diff --git a/examples/basic/.meteor/versions b/examples/basic/.meteor/versions new file mode 100644 index 00000000..8f8c5517 --- /dev/null +++ b/examples/basic/.meteor/versions @@ -0,0 +1,49 @@ +application-configuration@1.0.0 +autopublish@1.0.0 +autoupdate@1.0.0 +binary-heap@1.0.0 +blaze-tools@1.0.0 +blaze@1.0.0 +callback-hook@1.0.0 +check@1.0.0 +ctl-helper@1.0.0 +ctl@1.0.0 +deps@1.0.0 +ejson@1.0.0 +follower-livedata@1.0.0 +geojson-utils@1.0.0 +html-tools@1.0.0 +htmljs@1.0.0 +id-map@1.0.0 +insecure@1.0.0 +iron:controller@0.1.0 +iron:core@0.1.0 +iron:dynamic-template@0.1.0 +iron:layout@0.1.0 +iron:location@0.1.0 +iron:middleware-stack@0.1.0 +iron:router@0.9.0 +iron:url@0.1.0 +jquery@1.0.0 +json@1.0.0 +livedata@1.0.0 +logging@1.0.0 +meteor@1.0.0 +minifiers@1.0.0 +minimongo@1.0.0 +mongo-livedata@1.0.1 +observe-sequence@1.0.0 +ordered-dict@1.0.0 +random@1.0.0 +reactive-dict@1.0.0 +reload@1.0.0 +retry@1.0.0 +routepolicy@1.0.0 +session@1.0.0 +spacebars-compiler@1.0.0 +spacebars@1.0.0 +standard-app-packages@1.0.0 +templating@1.0.0 +ui@1.0.0 +underscore@1.0.0 +webapp@1.0.0 diff --git a/examples/basic/basic.html b/examples/basic/basic.html new file mode 100644 index 00000000..4c73c86d --- /dev/null +++ b/examples/basic/basic.html @@ -0,0 +1,42 @@ + + basic + + + + + + + + + + + + diff --git a/examples/basic/basic.js b/examples/basic/basic.js new file mode 100644 index 00000000..eb6ffecd --- /dev/null +++ b/examples/basic/basic.js @@ -0,0 +1,10 @@ +Router.route('/', function () { + // render the Home template with a custom data context + this.render('Home', {data: {title: 'My Title'}}); +}); + +// when you navigate to "/one" automatically render the template named "One". +Router.route('/one'); + +// when you navigate to "/two" automatically render the template named "Two". +Router.route('/two'); diff --git a/examples/data_not_found_plugin/.meteor/.gitignore b/examples/data_not_found_plugin/.meteor/.gitignore new file mode 100644 index 00000000..40830374 --- /dev/null +++ b/examples/data_not_found_plugin/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/examples/data_not_found_plugin/.meteor/identifier b/examples/data_not_found_plugin/.meteor/identifier new file mode 100644 index 00000000..3747f4b4 --- /dev/null +++ b/examples/data_not_found_plugin/.meteor/identifier @@ -0,0 +1 @@ +sfnw7e1pu12bl1xv8k6i \ No newline at end of file diff --git a/examples/data_not_found_plugin/.meteor/packages b/examples/data_not_found_plugin/.meteor/packages new file mode 100644 index 00000000..0d05e574 --- /dev/null +++ b/examples/data_not_found_plugin/.meteor/packages @@ -0,0 +1,11 @@ +# Meteor packages used by this project, one per line. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +standard-app-packages +autopublish +insecure +bootstrap +iron:router + diff --git a/examples/data_not_found_plugin/.meteor/release b/examples/data_not_found_plugin/.meteor/release new file mode 100644 index 00000000..621e94f0 --- /dev/null +++ b/examples/data_not_found_plugin/.meteor/release @@ -0,0 +1 @@ +none diff --git a/examples/data_not_found_plugin/.meteor/versions b/examples/data_not_found_plugin/.meteor/versions new file mode 100644 index 00000000..52508dcf --- /dev/null +++ b/examples/data_not_found_plugin/.meteor/versions @@ -0,0 +1,50 @@ +application-configuration@1.0.0 +autopublish@1.0.0 +autoupdate@1.0.0 +binary-heap@1.0.0 +blaze-tools@1.0.0 +blaze@1.0.0 +bootstrap@1.0.0 +callback-hook@1.0.0 +check@1.0.0 +ctl-helper@1.0.0 +ctl@1.0.0 +deps@1.0.0 +ejson@1.0.0 +follower-livedata@1.0.0 +geojson-utils@1.0.0 +html-tools@1.0.0 +htmljs@1.0.0 +id-map@1.0.0 +insecure@1.0.0 +iron:controller@0.1.0 +iron:core@0.1.0 +iron:dynamic-template@0.1.0 +iron:layout@0.1.2 +iron:location@0.1.0 +iron:middleware-stack@0.1.0 +iron:router@0.9.0 +iron:url@0.1.0 +jquery@1.0.0 +json@1.0.0 +livedata@1.0.0 +logging@1.0.0 +meteor@1.0.0 +minifiers@1.0.0 +minimongo@1.0.0 +mongo-livedata@1.0.1 +observe-sequence@1.0.0 +ordered-dict@1.0.0 +random@1.0.0 +reactive-dict@1.0.0 +reload@1.0.0 +retry@1.0.0 +routepolicy@1.0.0 +session@1.0.0 +spacebars-compiler@1.0.0 +spacebars@1.0.0 +standard-app-packages@1.0.0 +templating@1.0.0 +ui@1.0.0 +underscore@1.0.0 +webapp@1.0.0 diff --git a/examples/data_not_found_plugin/data_not_found_plugin.html b/examples/data_not_found_plugin/data_not_found_plugin.html new file mode 100644 index 00000000..6543264c --- /dev/null +++ b/examples/data_not_found_plugin/data_not_found_plugin.html @@ -0,0 +1,18 @@ + + data_not_found_hook + + + + + + + + diff --git a/examples/data_not_found_plugin/data_not_found_plugin.js b/examples/data_not_found_plugin/data_not_found_plugin.js new file mode 100644 index 00000000..b1ee23e8 --- /dev/null +++ b/examples/data_not_found_plugin/data_not_found_plugin.js @@ -0,0 +1,20 @@ +Router.configure({ + // optionally specify a not found template for the dataNotFound hook. + notFoundTemplate: 'DataNotFound' +}); + +Router.plugin('dataNotFound'); + +Router.route('/', { + name: 'home', + + data: function () { + + /** + * To render the normal template change to: + * + * return { title: 'Some title' }; + */ + return null; + } +}); diff --git a/examples/link_helpers/.meteor/.gitignore b/examples/link_helpers/.meteor/.gitignore new file mode 100644 index 00000000..40830374 --- /dev/null +++ b/examples/link_helpers/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/examples/link_helpers/.meteor/identifier b/examples/link_helpers/.meteor/identifier new file mode 100644 index 00000000..d809d89d --- /dev/null +++ b/examples/link_helpers/.meteor/identifier @@ -0,0 +1 @@ +19qsffs7evr6l8x9vyt \ No newline at end of file diff --git a/examples/link_helpers/.meteor/packages b/examples/link_helpers/.meteor/packages new file mode 100644 index 00000000..35fa0b57 --- /dev/null +++ b/examples/link_helpers/.meteor/packages @@ -0,0 +1,10 @@ +# Meteor packages used by this project, one per line. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +standard-app-packages +autopublish +insecure +iron:router + diff --git a/examples/link_helpers/.meteor/release b/examples/link_helpers/.meteor/release new file mode 100644 index 00000000..621e94f0 --- /dev/null +++ b/examples/link_helpers/.meteor/release @@ -0,0 +1 @@ +none diff --git a/examples/link_helpers/.meteor/versions b/examples/link_helpers/.meteor/versions new file mode 100644 index 00000000..8f8c5517 --- /dev/null +++ b/examples/link_helpers/.meteor/versions @@ -0,0 +1,49 @@ +application-configuration@1.0.0 +autopublish@1.0.0 +autoupdate@1.0.0 +binary-heap@1.0.0 +blaze-tools@1.0.0 +blaze@1.0.0 +callback-hook@1.0.0 +check@1.0.0 +ctl-helper@1.0.0 +ctl@1.0.0 +deps@1.0.0 +ejson@1.0.0 +follower-livedata@1.0.0 +geojson-utils@1.0.0 +html-tools@1.0.0 +htmljs@1.0.0 +id-map@1.0.0 +insecure@1.0.0 +iron:controller@0.1.0 +iron:core@0.1.0 +iron:dynamic-template@0.1.0 +iron:layout@0.1.0 +iron:location@0.1.0 +iron:middleware-stack@0.1.0 +iron:router@0.9.0 +iron:url@0.1.0 +jquery@1.0.0 +json@1.0.0 +livedata@1.0.0 +logging@1.0.0 +meteor@1.0.0 +minifiers@1.0.0 +minimongo@1.0.0 +mongo-livedata@1.0.1 +observe-sequence@1.0.0 +ordered-dict@1.0.0 +random@1.0.0 +reactive-dict@1.0.0 +reload@1.0.0 +retry@1.0.0 +routepolicy@1.0.0 +session@1.0.0 +spacebars-compiler@1.0.0 +spacebars@1.0.0 +standard-app-packages@1.0.0 +templating@1.0.0 +ui@1.0.0 +underscore@1.0.0 +webapp@1.0.0 diff --git a/examples/link_helpers/link_helpers.html b/examples/link_helpers/link_helpers.html new file mode 100644 index 00000000..08164091 --- /dev/null +++ b/examples/link_helpers/link_helpers.html @@ -0,0 +1,48 @@ + + link helpers + + + + + + + + + + + + diff --git a/examples/link_helpers/link_helpers.js b/examples/link_helpers/link_helpers.js new file mode 100644 index 00000000..16a6d284 --- /dev/null +++ b/examples/link_helpers/link_helpers.js @@ -0,0 +1,6 @@ +Router.route('/', function () { + this.render('Home'); +}); + +Router.route('/one'); +Router.route('/two'); diff --git a/examples/loading_plugin/.meteor/.gitignore b/examples/loading_plugin/.meteor/.gitignore new file mode 100644 index 00000000..40830374 --- /dev/null +++ b/examples/loading_plugin/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/examples/loading_plugin/.meteor/identifier b/examples/loading_plugin/.meteor/identifier new file mode 100644 index 00000000..a4ee0172 --- /dev/null +++ b/examples/loading_plugin/.meteor/identifier @@ -0,0 +1 @@ +9w4k9i1hb9dpiqyc1dh \ No newline at end of file diff --git a/examples/loading_plugin/.meteor/packages b/examples/loading_plugin/.meteor/packages new file mode 100644 index 00000000..c25f3baa --- /dev/null +++ b/examples/loading_plugin/.meteor/packages @@ -0,0 +1,11 @@ +# Meteor packages used by this project, one per line. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +standard-app-packages +autopublish +insecure +iron:router +bootstrap + diff --git a/examples/loading_plugin/.meteor/release b/examples/loading_plugin/.meteor/release new file mode 100644 index 00000000..621e94f0 --- /dev/null +++ b/examples/loading_plugin/.meteor/release @@ -0,0 +1 @@ +none diff --git a/examples/loading_plugin/.meteor/versions b/examples/loading_plugin/.meteor/versions new file mode 100644 index 00000000..52508dcf --- /dev/null +++ b/examples/loading_plugin/.meteor/versions @@ -0,0 +1,50 @@ +application-configuration@1.0.0 +autopublish@1.0.0 +autoupdate@1.0.0 +binary-heap@1.0.0 +blaze-tools@1.0.0 +blaze@1.0.0 +bootstrap@1.0.0 +callback-hook@1.0.0 +check@1.0.0 +ctl-helper@1.0.0 +ctl@1.0.0 +deps@1.0.0 +ejson@1.0.0 +follower-livedata@1.0.0 +geojson-utils@1.0.0 +html-tools@1.0.0 +htmljs@1.0.0 +id-map@1.0.0 +insecure@1.0.0 +iron:controller@0.1.0 +iron:core@0.1.0 +iron:dynamic-template@0.1.0 +iron:layout@0.1.2 +iron:location@0.1.0 +iron:middleware-stack@0.1.0 +iron:router@0.9.0 +iron:url@0.1.0 +jquery@1.0.0 +json@1.0.0 +livedata@1.0.0 +logging@1.0.0 +meteor@1.0.0 +minifiers@1.0.0 +minimongo@1.0.0 +mongo-livedata@1.0.1 +observe-sequence@1.0.0 +ordered-dict@1.0.0 +random@1.0.0 +reactive-dict@1.0.0 +reload@1.0.0 +retry@1.0.0 +routepolicy@1.0.0 +session@1.0.0 +spacebars-compiler@1.0.0 +spacebars@1.0.0 +standard-app-packages@1.0.0 +templating@1.0.0 +ui@1.0.0 +underscore@1.0.0 +webapp@1.0.0 diff --git a/examples/loading_plugin/loading_plugin.html b/examples/loading_plugin/loading_plugin.html new file mode 100644 index 00000000..ec5936b0 --- /dev/null +++ b/examples/loading_plugin/loading_plugin.html @@ -0,0 +1,21 @@ + + loading plugin + + + + + + + + diff --git a/examples/loading_plugin/loading_plugin.js b/examples/loading_plugin/loading_plugin.js new file mode 100644 index 00000000..67d95e32 --- /dev/null +++ b/examples/loading_plugin/loading_plugin.js @@ -0,0 +1,16 @@ +Router.configure({ + // optionally configure a template to use + // the default loading text to use is "Loading..." + loadingTemplate: 'Loading' +}); + +Router.plugin('loading', {only: 'home'}); + +Router.route('/', { + name: 'home', + waitOn: function () { + return { + ready: function () { return Session.get('ready'); } + }; + } +}); diff --git a/examples/render_helper/.meteor/.gitignore b/examples/render_helper/.meteor/.gitignore new file mode 100644 index 00000000..40830374 --- /dev/null +++ b/examples/render_helper/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/examples/render_helper/.meteor/identifier b/examples/render_helper/.meteor/identifier new file mode 100644 index 00000000..c50860b2 --- /dev/null +++ b/examples/render_helper/.meteor/identifier @@ -0,0 +1 @@ +17db6cj1aku57i1t2ra1u \ No newline at end of file diff --git a/examples/render_helper/.meteor/packages b/examples/render_helper/.meteor/packages new file mode 100644 index 00000000..35fa0b57 --- /dev/null +++ b/examples/render_helper/.meteor/packages @@ -0,0 +1,10 @@ +# Meteor packages used by this project, one per line. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. + +standard-app-packages +autopublish +insecure +iron:router + diff --git a/examples/render_helper/.meteor/release b/examples/render_helper/.meteor/release new file mode 100644 index 00000000..621e94f0 --- /dev/null +++ b/examples/render_helper/.meteor/release @@ -0,0 +1 @@ +none diff --git a/examples/render_helper/.meteor/versions b/examples/render_helper/.meteor/versions new file mode 100644 index 00000000..8f8c5517 --- /dev/null +++ b/examples/render_helper/.meteor/versions @@ -0,0 +1,49 @@ +application-configuration@1.0.0 +autopublish@1.0.0 +autoupdate@1.0.0 +binary-heap@1.0.0 +blaze-tools@1.0.0 +blaze@1.0.0 +callback-hook@1.0.0 +check@1.0.0 +ctl-helper@1.0.0 +ctl@1.0.0 +deps@1.0.0 +ejson@1.0.0 +follower-livedata@1.0.0 +geojson-utils@1.0.0 +html-tools@1.0.0 +htmljs@1.0.0 +id-map@1.0.0 +insecure@1.0.0 +iron:controller@0.1.0 +iron:core@0.1.0 +iron:dynamic-template@0.1.0 +iron:layout@0.1.0 +iron:location@0.1.0 +iron:middleware-stack@0.1.0 +iron:router@0.9.0 +iron:url@0.1.0 +jquery@1.0.0 +json@1.0.0 +livedata@1.0.0 +logging@1.0.0 +meteor@1.0.0 +minifiers@1.0.0 +minimongo@1.0.0 +mongo-livedata@1.0.1 +observe-sequence@1.0.0 +ordered-dict@1.0.0 +random@1.0.0 +reactive-dict@1.0.0 +reload@1.0.0 +retry@1.0.0 +routepolicy@1.0.0 +session@1.0.0 +spacebars-compiler@1.0.0 +spacebars@1.0.0 +standard-app-packages@1.0.0 +templating@1.0.0 +ui@1.0.0 +underscore@1.0.0 +webapp@1.0.0 diff --git a/examples/render_helper/render_router.html b/examples/render_helper/render_router.html new file mode 100644 index 00000000..222cf67d --- /dev/null +++ b/examples/render_helper/render_router.html @@ -0,0 +1,43 @@ + + render_router + + + +
+

My Application Container

+ + {{> Nav}} + +
+
+ {{> Router}} +
+
+
+ + + + + + + + + diff --git a/examples/render_helper/render_router.js b/examples/render_helper/render_router.js new file mode 100644 index 00000000..16a6d284 --- /dev/null +++ b/examples/render_helper/render_router.js @@ -0,0 +1,6 @@ +Router.route('/', function () { + this.render('Home'); +}); + +Router.route('/one'); +Router.route('/two'); diff --git a/lib/global_router.js b/lib/global_router.js new file mode 100644 index 00000000..e6d728f7 --- /dev/null +++ b/lib/global_router.js @@ -0,0 +1 @@ +Router = new Iron.Router; diff --git a/lib/helpers_client.js b/lib/helpers_client.js new file mode 100644 index 00000000..7d48f290 --- /dev/null +++ b/lib/helpers_client.js @@ -0,0 +1,99 @@ +/*****************************************************************************/ +/* Imports */ +/*****************************************************************************/ +var warn = Iron.utils.warn; +var DynamicTemplate = Iron.DynamicTemplate; + +/*****************************************************************************/ +/* UI Helpers */ +/*****************************************************************************/ + +/** + * Render the Router to a specific location on the page instead of the + * document.body. + */ +UI.registerHelper('Router', Template.__create__('Router', function () { + return Router.createView(); +})); + +/** + * Returns a relative path given a route name, data context and optional query + * and hash parameters. + */ +UI.registerHelper('pathFor', function (options) { + var opts = options && options.hash; + warn(opts, 'No options were passed to pathFor'); + + opts = opts || {}; + + var path = ''; + var query = opts.query; + var hash = opts.hash; + var routeName = opts.route; + var data = opts.data || this; + + var route = Router.routes[routeName]; + warn(route, "pathFor couldn't find a route named " + JSON.stringify(routeName)); + + if (route) + path = route.path(data, {query: query, hash: hash}); + + return path; +}); + +/** + * Returns a relative path given a route name, data context and optional query + * and hash parameters. + */ +UI.registerHelper('urlFor', function (options) { + var opts = options && options.hash; + warn(opts, 'No options were passed to urlFor'); + + opts = opts || {}; + var url = ''; + var query = opts.query; + var hash = opts.hash; + var routeName = opts.route; + var data = opts.data || this; + + var route = Router.routes[routeName]; + warn(route, "urlFor couldn't find a route named " + JSON.stringify(routeName)); + + if (route) + url = route.url(data, {query: query, hash: hash}); + + return url; +}); + +/** + * Create a link with optional content block. + * + * Example: + * {{#linkTo route="one" query="query" hash="hash" class="my-cls"}} + *
My Custom Link Content
+ * {{/linkTo}} + */ +UI.registerHelper('linkTo', Template.__create__('linkTo', function () { + var opts = DynamicTemplate.getInclusionArguments(this); + warn(opts, 'No options were passed to linkTo'); + + opts = opts || {}; + var path = ''; + var query = opts.query; + var hash = opts.hash; + var routeName = opts.route; + var data = opts.data || DynamicTemplate.getParentDataContext(this); + var route = Router.routes[routeName]; + + warn(route, "linkTo couldn't find a route named " + JSON.stringify(routeName)); + + if (route) + path = route.path(data, {query: query, hash: hash}); + + // anything that isn't one of our keywords we'll assume is an attributed + // intended for the tag + var attrs = _.omit(opts, 'route', 'query', 'hash', 'data'); + attrs.href = path; + + return HTML.A(attrs, this.templateContentBlock); +})); diff --git a/lib/hooks_client.js b/lib/hooks_client.js new file mode 100644 index 00000000..e3bf0a39 --- /dev/null +++ b/lib/hooks_client.js @@ -0,0 +1,57 @@ +/** + * The default anonymous loading template. + */ +var defaultLoadingTemplate = Template.__create__('DefaultLoadingTemplate', function () { + return 'Loading...'; +}); + +/** + * Automatically render a loading template into the main region if the + * controller is not ready (i.e. this.ready() is false). If no loadingTemplate + * is defined use some default text. + */ + +Router.hooks.loading = function () { + // if we're ready just pass through + if (this.ready()) { + this.next(); + return; + } + + var template = this.lookupOption('loadingTemplate'); + this.render(template || defaultLoadingTemplate); + this.renderRegions(); +}; + +/** + * The default anonymous data not found template. + */ +var defaultDataNotFoundTemplate = Template.__create__('DefaultDataNotFoundTemplate', function () { + return 'Data not found...'; +}); + +/** + * Render a "data not found" template if a global data function returns a falsey + * value + */ +Router.hooks.dataNotFound = function () { + if (!this.ready()) { + this.next(); + return; + } + + var data = this.lookupOption('data'); + var dataValue; + var template = this.lookupOption('notFoundTemplate'); + + if (data) { + if (!(dataValue = data())) { + this.render(template || defaultDataNotFoundTemplate); + this.renderRegions(); + return; + } + } + + // okay never mind just pass along now + this.next(); +}; diff --git a/lib/http_methods.js b/lib/http_methods.js new file mode 100644 index 00000000..38cbd2aa --- /dev/null +++ b/lib/http_methods.js @@ -0,0 +1,27 @@ +HTTP_METHODS = [ + 'get', + 'post', + 'put', + 'head', + 'delete', + 'options', + 'trace', + 'copy', + 'lock', + 'mkcol', + 'move', + 'purge', + 'propfind', + 'proppatch', + 'unlock', + 'report', + 'mkactivity', + 'checkout', + 'merge', + 'm-search', + 'notify', + 'subscribe', + 'unsubscribe', + 'patch', + 'search' +]; diff --git a/lib/plugins.js b/lib/plugins.js new file mode 100644 index 00000000..14bf47d7 --- /dev/null +++ b/lib/plugins.js @@ -0,0 +1,13 @@ +/** + * Simple plugin wrapper around the loading hook. + */ +Router.plugins.loading = function (router, options) { + router.onBeforeAction('loading', options); +}; + +/** + * Simple plugin wrapper around the dataNotFound hook. + */ +Router.plugins.dataNotFound = function (router, options) { + router.onBeforeAction('dataNotFound', options); +}; diff --git a/lib/route.js b/lib/route.js new file mode 100644 index 00000000..8f854c15 --- /dev/null +++ b/lib/route.js @@ -0,0 +1,195 @@ +var Url = Iron.Url; +var MiddlewareStack = Iron.MiddlewareStack; + +/*****************************************************************************/ +/* Both */ +/*****************************************************************************/ +Route = function (path, fn, options) { + var route = function (req, res, next) { + var controller = this; + route.dispatch(controller, req.url, { + req: req, + res: res, + next: next, + thisArg: controller + }); + } + + return ctor.call(this, route, path, fn, options); +}; + +var ctor = function (route, path, fn, options) { + if (typeof fn === 'object') { + options = fn; + fn = options.action; + } + + // extend the route function with properties from this instance and its + // prototype. + _.extend(route, this); + + // always good to have options + options = route.options = options || {}; + + // the main action function as well as any HTTP VERB action functions will go + // onto this stack. + route._actionStack = new MiddlewareStack; + + // any before hooks will go onto this stack to make sure they get executed + // before the action stack. + route._beforeStack = new MiddlewareStack; + route._beforeStack.append(route.options.onBeforeAction); + route._beforeStack.append(route.options.before); + + // after hooks get run after the action stack + route._afterStack = new MiddlewareStack; + route._afterStack.append(route.options.onAfterAction); + route._afterStack.append(route.options.after); + + // the original path + route._path = Url.normalize(path); + + // track which methods this route uses + route._methods = {}; + + if (fn) { + route._actionStack.push(route._path, fn, options); + } + + return route; +}; + +/** + * The name of the route is actually stored on the handler since a route is a + * function that has an unassignable "name" property. + */ +Route.prototype.getName = function () { + return this.handler && this.handler.name; +}; + +/** + * Returns an appropriate RouteController constructor the this Route. + * + * There are three possibilities: + * + * 1. controller option provided as a string on the route + * 2. a controller in the global namespace with the converted name of the route + * 3. a default RouteController + * + */ +Route.prototype.findControllerConstructor = function () { + var self = this; + + var resolve = function (name, opts) { + opts = opts || {}; + var C = Iron.utils.resolve(name); + if (typeof C === 'undefined' && opts.supressErrors !== true) + throw new Error("RouteController '" + name + "' is not defined."); + return C; + }; + + var convert = function (name) { + return self.router.toControllerName(name); + }; + + var result; + var name = this.getName(); + + // the controller was set directly + if (typeof this.options.controller === 'object') + return this.options.controller; + + // was the controller specified precisely by name? then resolve to an actual + // javascript constructor value + else if (typeof this.options.controller === 'string') + return resolve(this.options.controller); + + // otherwise do we have a name? try to convert the name to a controller name + // and resolve it to a value + else if (name && (result = resolve(convert(name), {supressErrors: true}))) + return result; + + // otherwise just use an anonymous route controller + else + return RouteController; +}; + + +/** + * Create a new controller for the route. + */ +Route.prototype.createController = function (options) { + options = options || {}; + var C = this.findControllerConstructor(); + var instance = new C(options); + instance.route = this; + return instance; +}; + +/** + * Dispatch into the route's middleware stack. + */ +Route.prototype.dispatch = function (controller, url, options) { + // call runRoute on the controller which will behave similarly to the previous + // version of IR. + controller._runRoute(this, url, options); +}; + +/** + * Returns a relative path for the route. + */ +Route.prototype.path = function (params, options) { + return this.handler.resolve(params, options); +}; + +/** + * Return a fully qualified url for the route, given a set of parmeters and + * options like hash and query. + */ +Route.prototype.url = function (params, options) { + var path = this.path(params, options); + var host = (options && options.host) || Meteor.absoluteUrl(); + + if (host.charAt(host.length-1) === '/'); + host = host.slice(0, host.length-1); + return host + path; +}; + +/** + * Return a params object for the route given a path. + */ +Route.prototype.params = function (path) { + return this.handler.params(path); +}; + +/** + * Add convenience methods for each HTTP verb. + * + * Example: + * var route = router.route('/item') + * .get(function () { }) + * .post(function () { }) + * .put(function () { }) + */ +HTTP_METHODS.forEach(function (method) { + Route.prototype[method] = function (fn) { + // track the method being used for OPTIONS requests. + this._methods[method] = true; + + this._actionStack.push(this._path, fn, { + // give each method a unique name so it doesn't clash with the route's + // name in the action stack + name: this.getName() + '_' + method.toLowerCase(), + method: method, + + // for now just make the handler where the same as the route, presumably a + // server route. + where: this.handler.where, + mount: false + }); + + return this; + }; +}); + +Iron.Route = Route; diff --git a/lib/route_controller.js b/lib/route_controller.js new file mode 100644 index 00000000..4a60d8d2 --- /dev/null +++ b/lib/route_controller.js @@ -0,0 +1,139 @@ +/*****************************************************************************/ +/* Imports */ +/*****************************************************************************/ +var Controller = Iron.Controller; +var Url = Iron.Url; +var MiddlewareStack = Iron.MiddlewareStack; + +/*****************************************************************************/ +/* RouteController */ +/*****************************************************************************/ +RouteController = Controller.extend({ + constructor: function (options) { + RouteController.__super__.constructor.apply(this, arguments); + this.options = options || {}; + this._onStopCallbacks = []; + this.init(options); + } +}); + +/** + * Returns an option value following an "options chain" which is this path: + * + * this (which includes the proto chain) + * this.options + * this.route.options + * this.router.options + */ +RouteController.prototype.lookupOption = function (key) { + + // "this" object or its proto chain + if (this[key]) + return this[key]; + + // this.options + if (_.has(this.options, key)) + return this.options[key]; + + // this.route.options + if (this.route && this.route.options && _.has(this.route.options, key)) + return this.route.options[key]; + + // this.router.options + if (this.router && this.router.options && _.has(this.router.options, key)) + return this.router.options[key]; +}; + +/** + * Returns an array of hook functions for the given hook names. Hooks are + * collected in this order: + * + * router global hooks + * route option hooks + * prototype of the controller + * this object for the controller + * + * For example, this.collectHooks('onBeforeAction', 'before') + * will return an array of hook functions where the key is either onBeforeAction + * or before. + * + * Hook values can also be strings in which case they are looked up in the + * Iron.Router.hooks object. + * + * TODO: Add an options last argument which can specify to only collect hooks + * for a particular environment (client, server or both). + */ +RouteController.prototype.collectHooks = function (/* hook1, alias1, ... */) { + var self = this; + var hookNames = _.toArray(arguments); + + var getHookValues = function (value) { + if (!value) + return []; + var lookupHook = self.router.lookupHook; + var hooks = _.isArray(value) ? value : [value]; + return _.map(hooks, function (h) { return lookupHook(h); }); + }; + + var collectInheritedHooks = function (ctor, hookName) { + var hooks = []; + + if (ctor.__super__) + hooks = hooks.concat(collectInheritedHooks(ctor.__super__.constructor, hookName)); + + return _.has(ctor.prototype, hookName) ? + hooks.concat(getHookValues(ctor.prototype[hookName])) : hooks; + }; + + var eachHook = function (cb) { + for (var i = 0; i < hookNames.length; i++) { + cb(hookNames[i]); + } + }; + + var routerHooks = []; + eachHook(function (hook) { + var name = self.route && self.route.getName(); + var hooks = self.router.getHooks(hook, name); + routerHooks = routerHooks.concat(hooks); + }); + + var protoHooks = []; + eachHook(function (hook) { + var hooks = collectInheritedHooks(self.constructor, hook); + protoHooks = protoHooks.concat(hooks); + }); + + var thisHooks = []; + eachHook(function (hook) { + if (_.has(self, hook)) { + var hooks = getHookValues(self[hook]); + thisHooks = thisHooks.concat(hooks); + } + }); + + var routeHooks = []; + if (self.route) { + eachHook(function (hook) { + var hooks = getHookValues(self.route.options[hook]); + routeHooks = routeHooks.concat(hooks); + }); + } + + var allHooks = routerHooks + .concat(routeHooks) + .concat(protoHooks) + .concat(thisHooks); + + return allHooks; +}; + +RouteController.prototype.runHooks = function (/* hook, alias1, ...*/ ) { + var hooks = this.collectHooks.apply(this, arguments); + for (var i = 0, l = hooks.length; i < l; i++) { + var h = hooks[i]; + h.call(this); + } +}; + +Iron.RouteController = RouteController; diff --git a/lib/route_controller_client.js b/lib/route_controller_client.js new file mode 100644 index 00000000..cd8ba1c4 --- /dev/null +++ b/lib/route_controller_client.js @@ -0,0 +1,227 @@ +/*****************************************************************************/ +/* Imports */ +/*****************************************************************************/ +var Controller = Iron.Controller; +var Url = Iron.Url; +var MiddlewareStack = Iron.MiddlewareStack; + +/*****************************************************************************/ +/* RouteController */ +/*****************************************************************************/ +/** + * Client specific initialization. + */ +RouteController.prototype.init = function (options) { + this._computation = null; +}; + +/** + * Let this controller run a dispatch process. This function will be called + * from the router. That way, any state associated with the dispatch can go on + * the controller instance. + */ +RouteController.prototype.dispatch = function (url, stack, options) { + if (this._computation && !this._computation.stopped) + throw new Error("RouteController computation is already running. Stop it first."); + + var self = this; + var result; + + options = _.extend({}, this.options, options || {}, {thisArg: this}); + + // break the computation chain with any parent comps + Deps.nonreactive(function () { + Deps.autorun(function (comp) { + self._computation = comp; + try { + self._isInDispatch = true; + result = stack.dispatch(url, options); + } finally { + self._isInDispatch = false; + } + }); + }); + + return result; +}; + +/** + * Run a route. When the router runs its middleware stack, it can run regular + * middleware functions or it can run a route. There should only one route + * object per path as where there may be many middleware functions. + * + * For example: + * + * "/some/path" => [middleware1, middleware2, route, middleware3] + * + * When a route is dispatched, it tells the controller to _runRoute so that + * the controller can controll the process. At this point we should already be + * in a dispatch so a computation should already exist. + */ +RouteController.prototype._runRoute = function (route, url, options) { + var self = this; + + if (!this._isInDispatch) + throw new Error("Can't call runRoute outside of a dispatch"); + + if (this._computation.firstRun && !RouteController._hasJustReloaded) { + this.runHooks('onRun', 'load'); + RouteController._hasJustReloaded = false; + } + + if (!this._computation.firstRun) { + this.runHooks('onRerun'); + } + + // see if there's a waitOn option we should process + var waitOn = this.lookupOption('waitOn'); + + // to simplify things waitOn should always be a function if it's defined. + // if you're overriding a parent waitOn, you'll need to make it a function + // that just returns undefined or something else. + if (typeof waitOn !== 'undefined' && !_.isFunction(waitOn)) + throw new Error("waitOn must be a function"); + + var waitOnResults; + if (waitOn) + waitOnResults = waitOn.call(this); + + if (typeof waitOnResults !== 'undefined') { + waitOnResults = _.isArray(waitOnResults) ? waitOnResults : [waitOnResults]; + + _.each(waitOnResults, function eachWaitOn (fnOrHandle) { + self.wait(fnOrHandle); + }); + } + + // start the rendering transaction so we record which regions were rendered + // into so we can clear the unused regions later. + this._layout.beginRendering(); + + this.layout(this.lookupOption('layoutTemplate'), { + data: this.lookupOption('data') + }); + + // dispatch into the "before" hook stack and the "action" stack making sure + // the before stack comes first. this lets a before hook stop downstream + // handlers by not calling this.next(). + var actionStack = new MiddlewareStack; + var beforeHooks = this.collectHooks('onBeforeAction', 'before'); + actionStack.append(beforeHooks, {where: 'client'}); + + // make sure the action stack has at least one handler on it that defaults + // to the 'action' method + if (route._actionStack.length === 0) + route._actionStack.push(route._path, 'action', route.options); + + actionStack = actionStack.concat(route._actionStack); + actionStack.dispatch(url, options); + + // run the after hooks. Note, at this point we're out of the middleware + // stack way of doing things. So after actions don't call this.next(). They + // run just like a regular hook. In contrast, before hooks have to call + // this.next() to progress to the next handler, just like Connect + // middleware. + this.next = function () {}; + this.runHooks('onAfterAction', 'after'); + + // clear any unused regions once we're done rendering. We put this in a + // Deps.afterFlush so that rendered regions from {{#contentFor}} blocks in our + // templates have a chance to register. + Deps.afterFlush(function () { + // if we're stopped don't do anything! + if (self.isStopped) + return; + + var usedRegions = self._layout.endRendering({flush: false}); + var allRegions = self._layout.regionKeys(); + var unusedRegions = _.difference(allRegions, usedRegions); + _.each(unusedRegions, function (r) { self._layout.clear(r); }); + }); +}; + +/** + * The default action for the controller simply renders the main template. + */ +RouteController.prototype.action = function () { + this.render(); +}; + +/** + * Returns the name of the main template for this controller. If no explicit + * value is found we will guess the name of the template. + */ +RouteController.prototype.lookupTemplate = function () { + return this.lookupOption('template') || + (this.router && this.router.toTemplateName(this.route.getName())); +}; + +/** + * The regionTemplates for the RouteController. + */ +RouteController.prototype.lookupRegionTemplates = function () { + return this.lookupOption('regionTemplates') || {}; +}; + +/** + * Overrides Controller.prototype.render to automatically render the + * controller's main template and region templates or just render a region + * template if the arguments are provided. + */ +RouteController.prototype.render = function (template, options) { + if (arguments.length === 0) { + var template = this.lookupTemplate(); + RouteController.__super__.render.call(this, template); + this.renderRegions(); + } else { + RouteController.__super__.render.call(this, template, options); + } +}; + +/** + * Render all region templates into their respective regions in the layout. + */ +RouteController.prototype.renderRegions = function () { + var self = this; + var regionTemplates = this.lookupOption('regionTemplates') || {}; + + // regionTemplates => + // { + // "MyTemplate": {to: 'MyRegion'} + // } + _.each(regionTemplates, function (opts, templateName) { + self.render(templateName, opts); + }); +}; + +/** + * Stop the RouteController. + */ +RouteController.prototype.stop = function () { + RouteController.__super__.stop.call(this); + + // if we started a rendering transaction, stop it now. + this._layout.endRendering({flush: false}); + + if (this._computation) + this._computation.stop(); + this.runHooks('onStop', 'unload'); +}; + +/** + * Just proxies to the go method of router. + * + * It used to have more significance. Keeping because people are used to it. + */ +RouteController.prototype.redirect = function () { + return this.router.go.apply(this.router, arguments); +}; + +if (Meteor._reload) { + // just register the fact that a migration _has_ happened + Meteor._reload.onMigrate('iron-router', function() { return [true, true]}); + + // then when we come back up, check if it it's set + var data = Meteor._reload.migrationData('iron-router'); + RouteController._hasJustReloaded = data; +} diff --git a/lib/route_controller_server.js b/lib/route_controller_server.js new file mode 100644 index 00000000..c96d773f --- /dev/null +++ b/lib/route_controller_server.js @@ -0,0 +1,82 @@ +/*****************************************************************************/ +/* Imports */ +/*****************************************************************************/ +var Fiber = Npm.require('fibers'); +var Controller = Iron.Controller; +var Url = Iron.Url; +var MiddlewareStack = Iron.MiddlewareStack; + +/*****************************************************************************/ +/* RouteController */ +/*****************************************************************************/ + +/** + * Server specific initialization. + */ +RouteController.prototype.init = function (options) {}; + +/** + * Let this controller run a dispatch process. This function will be called + * from the router. That way, any state associated with the dispatch can go on + * the controller instance. Note: no result returned from dispatch because its + * run inside its own fiber. Might at some point move the fiber stuff to a + * higher layer. + */ +RouteController.prototype.dispatch = function (url, stack, options) { + var self = this; + var result; + options = _.extend({}, this.options, options || {}, {thisArg: this}); + + try { + this._isInDispatch = true; + + Fiber(function () { + stack.dispatch(url, options); + }).run(); + } finally { + this._isInDispatch = false; + } +}; + +/** + * Run a route on the server. When the router runs its middleware stack, it + * can run regular middleware functions or it can run a route. There should + * only one route object per path as where there may be many middleware + * functions. + * + * For example: + * + * "/some/path" => [middleware1, middleware2, route, middleware3] + * + * When a route is dispatched, it tells the controller to _runRoute so that + * the controller can controll the process. At this point we should already be + * in a dispatch so a computation should already exist. + */ +RouteController.prototype._runRoute = function (route, url, options) { + var self = this; + + if (!this._isInDispatch) + throw new Error("Can't call runRoute outside of a dispatch"); + + this.runHooks('onRun', 'load'); + + // dispatch into the "before" hook stack and the "action" stack making sure + // the before stack comes first. this lets a before hook stop downstream + // handlers by not calling this.next(). + var actionStack = new MiddlewareStack; + var beforeHooks = this.collectHooks('onBeforeAction', 'before'); + actionStack.append(beforeHooks, {where: 'server'}); + + // make sure the action stack has at least one handler on it that defaults + // to the 'action' method + if (route._actionStack.length === 0) + route._actionStack.push(route.path, 'action', route.options); + + actionStack = actionStack.concat(route._actionStack); + actionStack.dispatch(url, options); + + // run the after hook. + this.next = function () {}; + this.runHooks('onAfterAction', 'after'); +}; + diff --git a/lib/router.js b/lib/router.js new file mode 100644 index 00000000..c9de38c5 --- /dev/null +++ b/lib/router.js @@ -0,0 +1,359 @@ +/*****************************************************************************/ +/* Imports */ +/*****************************************************************************/ +var MiddlewareStack = Iron.MiddlewareStack; +var Url = Iron.Url; +var Layout = Iron.Layout; +var warn = Iron.utils.warn; +var assert = Iron.utils.assert; + +Router = function (options) { + function router (req, res, next) { + router.dispatch(req.url, { + req: req, + res: res, + next: next, + thisArg: this + }); + } + + // the main router stack + router._stack = new MiddlewareStack; + + // for storing global hooks like before, after, etc. + router._globalHooks = {}; + + // backward compat and quicker lookup of Route handlers vs. regular function + // handlers. + router.routes = []; + + // to make sure we don't have more than one route per path + router.routes._byPath = {}; + + // always good to have options + this.configure.call(router, options); + + // add proto properties to the router function + _.extend(router, this); + + // let client and server side routing doing different things here + this.init.call(router, options); + + Meteor.startup(function () { + Meteor.defer(function () { + if (router.options.autoStart !== false) + router.start(); + }); + }); + + return router; +}; + +Router.prototype.init = function (options) {}; + +Router.prototype.configure = function (options) { + var self = this; + + options = options || {}; + + var toArray = function (value) { + if (!value) + return []; + + if (_.isArray(value)) + return value; + + return [value]; + }; + + // e.g. before: fn OR before: [fn1, fn2] + _.each(Iron.Router.HOOK_TYPES, function eachHookType (type) { + if (options[type]) { + _.each(toArray(options[type]), function eachHook (hook) { + self.addHook(type, hook); + }); + + delete options[type]; + } + }); + + this.options = this.options || {}; + _.extend(this.options, options); + + return this; +}; + +Router.prototype.use = function (path, fn, opts) { + opts = opts || {}; + opts.mount = true; + this._stack.push(path, fn, opts); + return this; +}; + +Router.prototype.route = function (path, fn, opts) { + if (typeof fn === 'object') { + opts = fn; + fn = opts.action; + } + + var route = new Route(path, fn, opts); + + opts = opts || {}; + + // make sure route doesn't already exist for this path + if (this.routes._byPath[route._path]) + throw new Error("A route for the path '" + route.path + "' already exists."); + else + this.routes._byPath[route._path] = route; + + // don't mount the route + opts.mount = false; + + // stack expects a function which is exactly what a new Route returns! + var handler = this._stack.push(path, route, opts); + + handler.route = route; + route.handler = handler; + route.router = this; + + this.routes.push(route); + + if (handler.name) { + if (this.routes[handler.name]) + throw new Error("A route with the name '" + handler.name + "' already exists"); + + this.routes[handler.name] = route; + } + + return route; +}; + +/** + * Find the first route for the given url and options. + */ +Router.prototype.findFirstRoute = function (url, options) { + for (var i = 0; i < this.routes.length; i++) { + if (this.routes[i].handler.test(url, options)) + return this.routes[i]; + } + + return null; +}; + +Router.prototype.path = function (routeName, params, options) { + var route = this.routes[routeName]; + warn(route, "You called Router.path for a route named " + JSON.stringify(routeName) + " but that route doesn't seem to exist. Are you sure you created it?"); + return route && route.path(params, options); +}; + +Router.prototype.url = function (routeName, params, options) { + var route = this.routes[routeName]; + warn(route, "You called Router.url for a route named " + JSON.stringify(routeName) + " but that route doesn't seem to exist. Are you sure you created it?"); + return route && route.url(params, options); +}; + +/** + * Create a new controller for a dispatch. + * + * @param {String} url The url for the dispatch so we can find the first route + * for the given url. + * @param {Object} [options] The options to pass to findFirstroute or the + * RouteController constructor. + */ +Router.prototype.createController = function (url, options) { + // see if there's a route for this url + var route = this.findFirstRoute(url, options); + var controller; + + options = options || {}; + + // so the controller works on our layout instead of creating a new one. + options.layout = this._layout; + + if (route) + // let the route decide what controller to use + controller = route.createController(options); + else + // create an anonymous controller + controller = new RouteController(options); + + controller.router = this; + return controller; +}; + +Router.prototype.setTemplateNameConverter = function (fn) { + this._templateNameConverter = fn; + return this; +}; + +Router.prototype.setControllerNameConverter = function (fn) { + this._controllerNameConverter = fn; + return this; +}; + +Router.prototype.toTemplateName = function (str) { + if (this._templateNameConverter) + return this._templateNameConverter(str); + else + return Iron.utils.classCase(str); +}; + +Router.prototype.toControllerName = function (str) { + if (this._controllerNameConverter) + return this._controllerNameConverter(str); + else + return Iron.utils.classCase(str) + 'Controller'; +}; + +/** + * + * Add a hook to all routes. The hooks will apply to all routes, + * unless you name routes to include or exclude via `only` and `except` options + * + * @param {String} [type] one of 'load', 'unload', 'before' or 'after' + * @param {Object} [options] Options to controll the hooks [optional] + * @param {Function} [hook] Callback to run + * @return {IronRouter} + * @api public + * + */ + +Router.prototype.addHook = function(type, hook, options) { + options = options || {}; + + var toArray = function (input) { + if (!input) + return []; + else if (_.isArray(input)) + return input; + else + return [input]; + } + + if (options.only) + options.only = toArray(options.only); + if (options.except) + options.except = toArray(options.except); + + var hooks = this._globalHooks[type] = this._globalHooks[type] || []; + hooks.push({options: options, hook: hook}); + return this; +}; + +/** + * If the argument is a function return it directly. If it's a string, see if + * there is a function in the Iron.Router.hooks namespace. Throw an error if we + * can't find the hook. + */ +Router.prototype.lookupHook = function (nameOrFn) { + var fn = nameOrFn; + + // if we already have a func just return it + if (_.isFunction(fn)) + return fn; + + // look up one of the out-of-box hooks like + // 'loaded or 'dataNotFound' if the nameOrFn is a + // string + if (_.isString(fn)) { + if (_.isFunction(Iron.Router.hooks[fn])) + return Iron.Router.hooks[fn]; + } + + // we couldn't find it so throw an error + throw new Error("No hook found named: ", nameOrFn); +}; + +/** + * + * Fetch the list of global hooks that apply to the given route name. + * Hooks are defined by the .addHook() function above. + * + * @param {String} [type] one of IronRouter.HOOK_TYPES + * @param {String} [name] the name of the route we are interested in + * @return {[Function]} [hooks] an array of hooks to run + * @api public + * + */ + +Router.prototype.getHooks = function(type, name) { + var self = this; + var hooks = []; + + _.each(this._globalHooks[type], function(hook) { + var options = hook.options; + + if (options.except && _.include(options.except, name)) + return []; + + if (options.only && ! _.include(options.only, name)) + return []; + + hooks.push(self.lookupHook(hook.hook)); + }); + + return hooks; +}; + +Router.HOOK_TYPES = [ + 'onRun', + 'onRerun', + 'onBeforeAction', + 'onAfterAction', + 'onStop', + + // not technically a hook but we'll use it + // in a similar way. This will cause waitOn + // to be added as a method to the Router and then + // it can be selectively applied to specific routes + 'waitOn', + + // legacy hook types but we'll let them slide + 'load', // onRun + 'before', // onBeforeAction + 'after', // onAfterAction + 'unload' // onStop +]; + +/** + * A namespace for hooks keyed by name. + */ +Router.hooks = {}; + + +/** + * A namespace for plugin functions keyed by name. + */ +Router.plugins = {}; + +/** + * Auto add helper mtehods for all the hooks. + */ + +Router.HOOK_TYPES.forEach(function (type) { + Router.prototype[type] = function (hook, options) { + this.addHook(type, hook, options); + }; +}); + +/** + * Add a plugin to the router instance. + */ +Router.prototype.plugin = function (nameOrFn, options) { + var func; + + if (typeof nameOrFn === 'function') + func = nameOrFn; + else if (typeof nameOrFn === 'string') + func = Iron.Router.plugins[nameOrFn]; + + if (!func) + throw new Error("No plugin found named " + JSON.stringify(nameOrFn)); + + // fn(router, options) + func.call(this, this, options); + + return this; +}; + +Iron.Router = Router; diff --git a/lib/router_client.js b/lib/router_client.js new file mode 100644 index 00000000..0816d265 --- /dev/null +++ b/lib/router_client.js @@ -0,0 +1,158 @@ +var MiddlewareStack = Iron.MiddlewareStack; +var Url = Iron.Url; +var Layout = Iron.Layout; +var assert = Iron.utils.assert; + +/** + * Client specific initialization. + */ +Router.prototype.init = function (options) { + var self = this; + + // the current RouteController from a dispatch + self._currentController = null; + + // the current() dep + self._currentDep = new Deps.Dependency; + + // the location computation + self._locationComputation = null; + + // the ui layout for the router + self._layout = new Layout({template: self.options.layoutTemplate}); + + // do we need to go to the server? + self._stack.onServerDispatch(function (url, options) { + self._redirectToServer(self, url, options); + }); + + Meteor.startup(function () { + setTimeout(function maybeAutoInsertRouter () { + if (self.options.autoRender !== false) + self.insert({el: document.body}); + }); + }); +}; + +/** + * Programmatically insert the router into document.body or a particular + * element with {el: 'selector'} + */ +Router.prototype.insert = function (options) { + this._layout.insert(options); + return this; +}; + +/** + * Returns a layout view that can be used in a UI helper to render the router + * to a particular place. + */ +Router.prototype.createView = function () { + return this._layout.create(); +}; + +Router.prototype._redirectToServer = function (controller, url, options) { + console.log('redirect to server'); + window.location = url; +}; + +Router.prototype.dispatch = function (url, options) { + options = options || {}; + + var self = this; + var controller = this.createController(url, options); + var result; + var out = options.next || function () {}; + + options.thisArg = controller; + url = controller.url = controller.path = Url.normalize(url); + + options.next = function (err) { + // on the client if we get an error we'll just throw it otherwise call out + // to the next function. + if (err) + throw err; + else + out(err); + }; + + if (this._currentController) + this._currentController.stop(); + + this._currentController = controller; + + result = controller.dispatch(url, self._stack, options); + + if (this._currentController == controller) + this._currentDep.changed(); + + return result; +}; + +/** + * The current controller object. + */ +Router.prototype.current = function () { + this._currentDep.depend(); + return this._currentController; +}; + + +/** + * Start reacting to location changes. + */ +Router.prototype.start = function () { + var self = this; + self._locationComputation = Deps.autorun(function (c) { + // grab the location and + var loc = Iron.Location.get(); + // need more information here like results + var controller = self.dispatch(loc.href, loc.state); + + // if we're going to the server cancel the url change + if (controller.isHandledOnServer()) + loc.cancelUrlChange(); + }); +}; + +/** + * Stop all computations and put us in a not started state. + */ +Router.prototype.stop = function () { + if (!this._isStarted) + return; + + if (this._locationComputation) + this._locationComputation.stop(); + + if (this._currentController) + this._currentController.stop(); + + this._isStarted = false; +}; + +/** + * Go to a given path or route name, optinally pass parameters and options. + * + * Example: + * router.go('itemsShowRoute', {_id: 5}, {hash: 'frag', query: 'string}); + */ +Router.prototype.go = function (routeNameOrPath, params, options) { + var self = this; + var isPath = /^\/|http/; + var path; + + if (isPath.test(routeNameOrPath)) { + // it's a path! + path = routeNameOrPath; + } else { + // it's a route name! + var route = self.routes[routeNameOrPath]; + assert(route, "No route found named " + JSON.stringify(routeNameOrPath)); + path = route.path(params, options); + } + + // let Iron Location handle it and we'll pick up the change in + // Iron.Location.get() computation. + Iron.Location.go(path, options); +}; diff --git a/lib/router_server.js b/lib/router_server.js new file mode 100644 index 00000000..0d7f9684 --- /dev/null +++ b/lib/router_server.js @@ -0,0 +1,83 @@ +var env = process.env.NODE_ENV || 'development'; + +/** + * Server specific initialization. + */ +Router.prototype.init = function (options) {}; + +/** + * Add the router to the server connect handlers. + */ +Router.prototype.start = function () { + WebApp.connectHandlers.use(this); +}; + +/** + * Create a new controller and dispatch into the stack. + */ +Router.prototype.dispatch = function (url, options) { + var self = this; + + options = options || {}; + + // assumes there is only one router + var controller = this.createController(url, options); + options.thisArg = controller; + + // save away the original + var done = options.next; + + options.next = function (err) { + var res = this.response; + var req = this.request; + var msg; + + if (err) { + if (res.statusCode < 400) + res.statusCode = 500; + + if (err.status) + res.statusCode = err.status; + + if (env === 'development') + msg = (err.stack || err.toString()) + '\n'; + else + //XXX get this from standard dict of error messages? + msg = 'Server error.'; + + console.error(err.stack || err.toString()); + + if (res.headersSent) + return req.socket.destroy(); + + res.setHeader('Content-Type', 'text/html'); + res.setHeader('Content-Length', Buffer.byteLength(msg)); + if (req.method === 'HEAD') + return res.end(); + res.end(msg); + return; + } + + // if there are no client or server handlers for this dispatch + // then send a 404. + if (!controller.isHandled() && !controller.isHandledOnClient()) { + res.statusCode = 404; + res.setHeader('Content-Type', 'text/html'); + msg = req.method + ' ' + req.originalUrl + ' not found.'; + console.error(msg); + if (req.method == 'HEAD') + return res.end(); + res.end(msg + '\n'); + return; + } + + // nothing else handled? punt out to the next Connect middleware handler + // which is probably Meteor. + // XXX what happens if we call this.response.end and then this calls into + // the next middleware handler? + if (done) + done(err); + }; + + controller.dispatch(url, this._stack, options); +}; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 00000000..54e6af8d --- /dev/null +++ b/lib/utils.js @@ -0,0 +1 @@ +Please see https://github.com/EventedMind/iron-router#filing-issues-and-contributing for information about contributing. diff --git a/notes.txt b/notes.txt new file mode 100644 index 00000000..dcb32046 --- /dev/null +++ b/notes.txt @@ -0,0 +1,61 @@ +XXX testing +XXX readme and updated examples +XXX contributor guidelines update + + proper error handling and error testing on dispatch + controller hooks to route middleware + client view api + global data for layout and render data + XXX backward compat for paths vs. names in handlers and routes! + + waitOn + onRun, onStop, onAfterAction, onRerun hooks + _hasJustReloaded + server side _runRoute + http verbs + Iron.Location + - clean up and fix bugs + - integrate with iron router + attach router + WebApp.connectHandlers.use + Iron.Location + fix wildcard params issue (test failing) +: put compiled url onto route so we can call resolve, params, etc on the + UI helpers + renderRouter + pathFor + urlFor + linkTo + opt out + + +Breaking Changes: +- waitOn is not concatenated. first one we find we use. +- onData hook is gone since it doesn't really have meaning anymore +- hooks now don't have any pause method use next() instead. +- no automatic error throwing when route not found on client. The user can do + this by putting a use() at the end. +- Router does not automatically start now. You must do it manually. + +Feature Tracking: +- route names are inferred from the path now + '/items/' -> Items +- simpler function based actions +- more granular data contexts (from code or templates) +- contentFor is a lot more powerful +- stateful controllers with get/set methods +- default data context for layout, or individual data contexts for regions. +- onRerun runs any time controller runs again either because of hot code push or + because of a dep changed. +- 404 tracked on server. if no routes defined the app is not even served. +- 500 errors on server routes. +- set data contexts in layout or render directly. no more setData getData +- ie 8/9 support for hash urls + +Proposed Features: +- hooks that run on client/server or both. For now, just use if block +- define "where" options for hooks so they can run on client/server or both +- server OPTIONS requests +- attach multiple routers, allow mounting a completely new router at a different + location. +- allow multiple routers diff --git a/package.js b/package.js new file mode 100644 index 00000000..293e9625 --- /dev/null +++ b/package.js @@ -0,0 +1,67 @@ +Package.describe({ + name: 'router', + summary: 'Routing specifically designed for Meteor', + version: "0.9.0", + githubUrl: "https://github.com/eventedmind/iron-router" +}); + +Package.on_use(function (api) { + // meteor dependencies + api.use('underscore'); + api.use('webapp', 'server'); + api.use('deps', 'client'); + api.use('ui'); + api.use('templating'); + + // main namespace and utils + api.use('iron:core'); + api.imply('iron:core'); + + // connect like middleware stack for client/server + api.use('iron:middleware-stack'); + + // client and server side url utilities and compiling + api.use('iron:url'); + + // for reactive urls and pushState in the browser + api.use('iron:location'); + + // for RouteController which inherits from this + api.use('iron:controller'); + + api.addFiles('lib/utils.js'); + api.addFiles('lib/http_methods.js'); + + + api.addFiles('lib/route_controller.js'); + api.addFiles('lib/route_controller_server.js', 'server'); + api.addFiles('lib/route_controller_client.js', 'client'); + + api.addFiles('lib/route.js'); + + api.addFiles('lib/router.js'); + api.addFiles('lib/router_client.js', 'client'); + api.addFiles('lib/router_server.js', 'server'); + + api.addFiles('lib/hooks_client.js', 'client'); + + api.addFiles('lib/helpers_client.js', 'client'); + + api.addFiles('lib/plugins.js'); + + api.addFiles('lib/global_router.js'); + + // symbol exports + api.export('Router'); +}); + +Package.on_test(function (api) { + api.use('iron:router'); + api.use('tinytest'); + api.use('test-helpers'); + + api.addFiles('test/helpers.js'); + api.addFiles('test/route_test.js'); + api.addFiles('test/router_test.js'); + api.addFiles('test/route_controller_test.js'); +}); diff --git a/test/helpers.js b/test/helpers.js new file mode 100644 index 00000000..c9696fc5 --- /dev/null +++ b/test/helpers.js @@ -0,0 +1 @@ +Router.configure({autoStart: false, autoRender: false}); diff --git a/test/route_controller_test.js b/test/route_controller_test.js new file mode 100644 index 00000000..e15d458c --- /dev/null +++ b/test/route_controller_test.js @@ -0,0 +1,37 @@ +Tinytest.add('RouteController - collectHooks', function (test) { + var router = new Iron.Router({autoStart: false, autoRender: false}); + + router.configure({ + onBeforeAction: function routerOnBeforeAction() {}, + before: function routerBefore() {} + }); + + var C = Iron.RouteController.extend({ + onBeforeAction: function protoOnBeforeAction() {}, + before: function protoBefore() {} + }); + + var route = router.route('/', { + controller: C, + onBeforeAction: function routeOnBeforeAction() {}, + before: function routeBefore() {} + }); + + Iron.Router.hooks.testHook = function () {}; + + // create some proto hooks + var c = new C; + c.router = router; + c.route = route; + + var hooks = c.collectHooks('onBeforeAction', 'before'); + + var hookNames = _.map(hooks, function (h) { return h.name; }); + + test.equal(hookNames[0], 'routerOnBeforeAction', 'router onBeforeAction'); + test.equal(hookNames[1], 'routerBefore', 'router before'); + test.equal(hookNames[2], 'routeOnBeforeAction', 'route onBeforeAction'); + test.equal(hookNames[3], 'routeBefore', 'route before'); + test.equal(hookNames[4], 'protoOnBeforeAction', 'proto onBeforeAction'); + test.equal(hookNames[5], 'protoBefore', 'proto before'); +}); diff --git a/test/route_test.js b/test/route_test.js new file mode 100644 index 00000000..9c7640bd --- /dev/null +++ b/test/route_test.js @@ -0,0 +1,24 @@ +var Route = Iron.Route; +var RouteController = Iron.RouteController; + +Tinytest.add('Route - findControllerConstructor', function (test) { + var global = Iron.utils.global; + var route; + var router = new Iron.Router({autoStart: false, autoRender: false}); + + global.FooController = RouteController.extend({ + }); + + route = new Route('/foo'); + route.router = router; + route.handler = {name: 'Foo'}; + test.equal(route.findControllerConstructor(), global.FooController); + + route = new Route('/bar', {controller: 'FooController'}); + route.router = router; + test.equal(route.findControllerConstructor(), global.FooController); + + route = new Route('/bar'); + route.router = router; + test.equal(route.findControllerConstructor(), Iron.RouteController); +}); diff --git a/test/router_test.js b/test/router_test.js new file mode 100644 index 00000000..f9f66283 --- /dev/null +++ b/test/router_test.js @@ -0,0 +1,77 @@ +Tinytest.add('Router - createController', function (test) { + test.ok(); +}); + +Tinytest.add('Router - dispatch and current', function (test) { + var calls = []; + var call; + var origDispatch = Iron.RouteController.prototype.dispatch; + + Iron.RouteController.prototype.dispatch = function (url, stack, opts) { + calls.push({ + opts: opts, + thisArg: this, + url: url, + stack: stack + }); + }; + + try { + var router = new Iron.Router({autoRender: false, autoStart: false}); + var req = {url: '/test'}, res = {}, next = function () {}; + var current; + + if (Meteor.isClient) { + Deps.autorun(function (c) { + current = router.current(); + }); + + router(req, res, next); + + test.equal(calls.length, 1, 'RouteController dispatch method called'); + call = calls[0]; + test.equal(call.url, '/test', 'dispatch url is set'); + test.instanceOf(call.thisArg, Iron.RouteController, 'thisArg is a RouteController'); + test.instanceOf(call.stack, Iron.MiddlewareStack, 'stack is a MiddlewareStack'); + + var opts = call.opts; + test.equal(opts.req, req, 'options request is set'); + test.equal(opts.res, res, 'options response is set'); + test.instanceOf(opts.thisArg, Iron.RouteController, 'thisArg set to controller'); + + test.isNull(current, 'current is null until a flush'); + Deps.flush(); + test.instanceOf(current, Iron.RouteController, 'current is instance of Iron.RouteController'); + + var oldCurrent = current; + + var stopped = false; + oldCurrent.stop = function () { stopped = true; }; + + router(req, res, next); + test.isTrue(stopped, 'previous controller stopped'); + Deps.flush(); + test.isTrue(oldCurrent !== current, 'current controller is not the old controller'); + } + + if (Meteor.isServer) { + router(req, res, next); + + test.equal(calls.length, 1, 'RouteController dispatch method called'); + call = calls[0]; + test.equal(call.url, '/test', 'dispatch url is set'); + test.instanceOf(call.thisArg, Iron.RouteController, 'thisArg is a RouteController'); + test.instanceOf(call.stack, Iron.MiddlewareStack, 'stack is a MiddlewareStack'); + + var opts = call.opts; + test.equal(opts.req, req, 'options request is set'); + test.equal(opts.res, res, 'options response is set'); + test.instanceOf(opts.thisArg, Iron.RouteController, 'thisArg set to controller'); + } + } finally { + Iron.RouteController.prototype.dispatch = origDispatch; + } +}); + +Tinytest.add('Router - dispatch error handling', function (test) { +}); diff --git a/versions.json b/versions.json new file mode 100644 index 00000000..b5ac88c1 --- /dev/null +++ b/versions.json @@ -0,0 +1,167 @@ +{ + "dependencies": [ + [ + "application-configuration", + "1.0.0" + ], + [ + "binary-heap", + "1.0.0" + ], + [ + "blaze", + "1.0.0" + ], + [ + "blaze-tools", + "1.0.0" + ], + [ + "callback-hook", + "1.0.0" + ], + [ + "check", + "1.0.0" + ], + [ + "deps", + "1.0.0" + ], + [ + "ejson", + "1.0.0" + ], + [ + "follower-livedata", + "1.0.0" + ], + [ + "geojson-utils", + "1.0.0" + ], + [ + "html-tools", + "1.0.0" + ], + [ + "htmljs", + "1.0.0" + ], + [ + "id-map", + "1.0.0" + ], + [ + "iron:controller", + "0.1.0" + ], + [ + "iron:core", + "0.1.0" + ], + [ + "iron:dynamic-template", + "0.1.0" + ], + [ + "iron:layout", + "0.1.2" + ], + [ + "iron:location", + "0.1.0" + ], + [ + "iron:middleware-stack", + "0.1.0" + ], + [ + "iron:url", + "0.1.0" + ], + [ + "jquery", + "1.0.0" + ], + [ + "json", + "1.0.0" + ], + [ + "livedata", + "1.0.0" + ], + [ + "logging", + "1.0.0" + ], + [ + "meteor", + "1.0.0" + ], + [ + "minifiers", + "1.0.0" + ], + [ + "minimongo", + "1.0.0" + ], + [ + "mongo-livedata", + "1.0.1" + ], + [ + "observe-sequence", + "1.0.0" + ], + [ + "ordered-dict", + "1.0.0" + ], + [ + "random", + "1.0.0" + ], + [ + "reactive-dict", + "1.0.0" + ], + [ + "retry", + "1.0.0" + ], + [ + "routepolicy", + "1.0.0" + ], + [ + "spacebars", + "1.0.0" + ], + [ + "spacebars-compiler", + "1.0.0" + ], + [ + "templating", + "1.0.0" + ], + [ + "ui", + "1.0.0" + ], + [ + "underscore", + "1.0.0" + ], + [ + "webapp", + "1.0.0" + ] + ], + "pluginDependencies": [], + "toolVersion": "meteor-tool@1.0.4", + "format": "1.0" +} \ No newline at end of file