├── .gitignore
├── .npmignore
├── .travis.yml
├── History.md
├── LICENSE
├── README.md
├── bin
├── template
│ ├── History.md
│ ├── LICENSE
│ ├── README.md
│ ├── server.js
│ ├── server_coffee.js
│ ├── test
│ │ ├── timbits-test.coffee
│ │ └── timbits-test.js
│ ├── timbit.coffee
│ └── timbit.js
└── timbits
├── examples
├── helpers
│ └── hello.js
├── server-redis.coffee
├── server.js
├── timbits
│ ├── cherry.js
│ ├── chocolate.js
│ ├── dutchie.js
│ ├── plain.js
│ └── post.js
└── views
│ ├── cherry
│ └── default.hjs
│ ├── chocolate
│ ├── alternate-view.hjs
│ └── default.hjs
│ ├── plain
│ └── default.hjs
│ └── post
│ └── default.hjs
├── lib
├── templates
│ ├── help.hjs
│ ├── test.hjs
│ └── timbit-help.hjs
└── timbits.js
├── package.json
├── resources
├── images
│ ├── accept.png
│ ├── brick_add.png
│ ├── cancel.png
│ ├── error.png
│ └── eye.png
├── javascript
│ ├── timbits.csi.js
│ └── timbits.csi.min.js
└── stylesheets
│ └── timbits.css
└── test
└── timbits-test.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | *.log
4 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .git*
3 | *.log
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 0.10
4 | notifications:
5 | email:
6 | recipients:
7 | - smrt@postmedia.com
8 |
--------------------------------------------------------------------------------
/History.md:
--------------------------------------------------------------------------------
1 | 0.7.3 / 2014-12-08
2 | ==================
3 | * Removed use of twitter API v1.0 in templates generated by 'timbits g' and README or examples
4 | * Added support for all methods on timbit endpoints, default support for GET enabled for backward compatibility
5 | * Example timbit added to demonstrate http POST method use
6 | * Testing updated to check and use GET/POST depending on which the timbit supports.
7 |
8 | 0.7.2 / 2014-05-12
9 | ==================
10 | * Dependency update to allow pantry v0.7.x
11 | * Dependency update to allow request v2.36.x
12 | * Dependency update to allow express v3.10.x
13 | * Dependency update to allow hjs v0.0.6
14 | * Dependency update to allow hogan.js v3.0.x
15 | * Dependency update to allow run v1.2.x
16 | * Dependency update to allow mocha v1.20.x
17 | * Dependency update to allow should v4.0.x
18 | * Examples - added parameter and example help for dutchie timbit
19 | * Added dependency for should-http v0.0.2 augments should v4.x
20 | * Updated node engine support from 0.8.x to up to 0.10.x
21 | * Updated npm to 1.x
22 | * Changed test to require '-c' or '--coffee' to use coffee-script
23 |
24 | 0.7.1 / 2013-07-16
25 | ==================
26 | * Dependency update to allow pantry v0.5.x
27 | * Now uses Wordpress API instead of Twitter for examples
28 |
29 | 0.7.0 / 2013-06-05
30 | ==================
31 | * 0.7.x series should be considered unstable. stable version will be released as 0.8.x
32 | * major rewrite of Timbits in JavaScript (formally CoffeeScript)
33 | * generates JavaScript files by default (CoffeeScript is optional)
34 | * default view engine is now Hogan (formally CoffeeKup)
35 | * dynamic helpers are no longer supported
36 | * sessions are not longer enabled by default
37 | * now uses Winston for logging (formally coloured-log)
38 | * updated documentation to reflect changes
39 |
40 | 0.6.7 / 2012-11-28
41 | ==================
42 | * added trim function when processing environment key/value pairs, fixes issue on Windows when multiple environment variables are concatenated in same string.
43 |
44 | 0.6.6 / 2012-11-08
45 | ==================
46 | * renamed csi libraries to match best practices (dot instead of dash)
47 |
48 | 0.6.5 / 2012-10-23
49 | ==================
50 | * client rendering library revised. no longer uses namespaced includes
51 |
52 | 0.6.4 / 2012-10-23
53 | ==================
54 |
55 | * Add support for responsive client side includes via media attribute
56 | * Modified production flags to return 404 when feature has been disabled
57 |
58 | 0.6.3 / 2012-10-22
59 | ==================
60 |
61 | * Supports new configuration options which allow you to disable automated discovery, help, tests, and json views.
62 | * no longer uses our own custom jsonp-filter package
63 |
64 | 0.6.2 / 2012-10-22
65 | ==================
66 |
67 | * Rewrite of timbits command line in js
68 | * No longer requires coffee-script, mocha or runjs packages to be installed globally
69 | * for projects created via "timbits new", generated package.json will lock down the timbits dependency to the version used to create the project. To upgrade to a newer version of timbits, you'll need to adjust the dependency first
70 | * Added support for numerous mocha options to "timbits test" command
71 |
72 | 0.6.1 / 2012-10-16
73 | ==================
74 |
75 | * Updated read me to reflect global dependencies for command line use of timbits
76 | * Modified spawn of child process to use the node-which lib for proper PATHEXTS of executables on Windows and Linux
77 |
78 | 0.6.0 / 2012-10-05
79 | ==================
80 |
81 | * Updated dependencies to latest libraries
82 | * Now requires node v0.8.x
83 | * timbits are now loaded before server is started (instead of async)
84 | * Switch to mocha for testing (was using vows)
85 | * Completely revised automated testing with option for more extensive tests
86 | * Added test option to timbits command line
87 | * timbits s[erver] and t[est] will now load environment variables from a .env file if present
88 |
89 | 0.5.14 / 2012-10-05
90 | ===================
91 | * Removed connect -esi, -assets, -less as requirements (but you can still add them to your project manually if needed)
92 |
93 | 0.5.13 / 2012-10-05
94 | ===================
95 |
96 | * Lock down dependency versions prior to v0.6.x
97 |
98 | 0.5.12 / 2012-06-28
99 | ===================
100 |
101 | * Added alias name support to timbit parameters
102 |
103 | 0.5.10 / 2012-06-07
104 | ===================
105 |
106 | * Added error handling to fetch helper method
107 |
108 | 0.5.9 / 2012-05-28
109 | ==================
110 |
111 | * Better handling of errors when eating or rendering
112 | * Removed auto-capitalization css from help views
113 |
114 | 0.5.8 / 2012-05-24
115 | ==================
116 |
117 | * Corrected method of returning remove render JSON
118 |
119 | 0.5.7 / 2012-05-23
120 | ==================
121 |
122 | * Added support for onload event to client side esi/csi processing
123 | * Allow for $(QUERY\_STRING{'name'}) syntax in client side esi/csi processing
124 |
125 | 0.5.6 / 2012-05-22
126 | ==================
127 |
128 | * Added ability to access to pantry directly from within a Timbit
129 |
130 | 0.5.5 / 2012-05-18
131 | ==================
132 |
133 | * Added the ability render timbits client side via JSONP and callback parameter
134 | * Removed the automatic insert of pantry URIs into context due to security concerns
135 |
136 | 0.5.4 / 2012-04-25
137 | ==================
138 |
139 | * Added the ability to specify the name of the default view via .defaultView property
140 |
141 | 0.5.3 / 2012-04-16
142 | ==================
143 |
144 | * Fixed bug with minimum node version for previous changes that require node 0.6.1 or higher
145 | * Added limitation to express version installed < 3.0 as this is in alpha and breaks timbits
146 |
147 | 0.5.2 / 2012-04-12
148 | ==================
149 |
150 | * Fixed bug with making directories in bin/timbits
151 |
152 | 0.5.1 / 2012-04-12
153 | ==================
154 |
155 | * Changes to bin/timbits for CoffeScript v.1.3.x support
156 |
157 | 0.5.0 / 2012-03-21
158 | ==================
159 |
160 | * Unstable experimental release (uses unstable pantry v0.3.x)
161 | * Exposes pantry for additional configuration and use of optional storage engines
162 |
163 | 0.4.2 / 2012-03-21
164 | ==================
165 |
166 | * Updated package to prevent use of pantry > 0.3.0
167 |
168 | 0.4.1 / 2012-02-21
169 | ==================
170 |
171 | * Added support for new config.base parameter to allow nested timbit servers
172 | * timbits command line updated to use OS agnostic copy
173 | * npm init and install removed from command line (doesn't work on windows)
174 |
175 | 0.4.0 / 2012-02-02
176 | ==================
177 |
178 | * Removed client side rendering (for now)
179 | * Switched out kitkat for vows
180 | * Supports alternate view engines
181 | * No error if helpers aren't defined (missing folder)
182 |
183 | 0.3.3 / 2011-11-23
184 | ==================
185 |
186 | * ignore empty parameter values
187 |
188 | 0.3.2 / 2011-11-18
189 | ==================
190 |
191 | * added support for server-side less compilation
192 |
193 | 0.3.1 / 2011-11-10
194 | ==================
195 |
196 | * added support for json directory of available timbits
197 | * Timbit fetch method will store pantry result in array if context contains existing entry
198 | * Timbit fetch method will store the requested uri in context\[name_uri\] (or array if context contains existing entry)
199 | * added initial support for express sessions
200 | * routing now works for both get and post methods
201 |
202 | 0.3.0 / 2011-10-17
203 | ==================
204 |
205 | * initial support for dynamic helpers
206 |
207 | 0.2.0 / 2011-10-17
208 | ==================
209 |
210 | * official stable release of v0.2
211 |
212 | 0.2.0beta5 / 2011-10-04
213 | =======================
214 |
215 | * complete rewrite of client side rendering
216 |
217 | 0.2.0beta4 / 2011-09-15
218 | =======================
219 |
220 | * upgraded pantry to v0.2.0beta2
221 | * changed view_base to viewBase
222 | * added parameter data type validation
223 | * added downstream caching headers via maxAge
224 | * npm init is now run after new project has been generated
225 | * parameters are converted to lower case to ensure they are not case sensitive
226 | * fixed bug with conflicting path variable
227 | * added append to body if no timbit_id is provided when rendering client side
228 |
229 | 0.2.0beta3 / 2011-09-14
230 | =======================
231 |
232 | * upgraded pantry to v0.2.0beta
233 | * fixed issue with test page host name. Closes #8
234 |
235 | 0.2.0beta2 / 2011-09-13
236 | =======================
237 |
238 | * new projects now depend on installed version of timbits
239 | * package.json for new projects now include project name
240 | * support for timbits -v parameter
241 | * added Timbit.log for logging/debug support
242 | * new projects now support continuos testing via kitkat
243 |
244 | 0.2.0beta / 2011-09-09
245 | ======================
246 |
247 | * working towards a stable production 0.2.0 release
248 |
249 | 0.1.3 / 2011-09-09
250 | ==================
251 |
252 | * command line for new projects and code generation
253 | * dynamic test pages
254 | * upgraded view engine to CoffeeKup 0.3.0
255 | * easier sharing of views between timbits via Timbit.view_base
256 | * revised fetch method by removing 'key' parameter
257 | * support for Timbits created in JavaScript
258 |
259 | 0.1.2 / 2011-08-25
260 | ==================
261 |
262 | * dynamic help has been styled
263 | * support for client side rendering
264 | * initial support for automated testing
265 | * better logging
266 |
267 | 0.1.1 / 2011-08-23
268 | ==================
269 |
270 | * request and response separated from context (to support optional client side rendering in a later release)
271 | * parameter validation
272 | * dynamic help pages
273 | * customized routes are no longer supported
274 | * examples updated to reflect changes
275 |
276 | 0.1.0 / 2011-08-22
277 | ==================
278 |
279 | * Official 0.1 release
280 |
281 | 0.0.7 / 2011-08-18
282 | ==================
283 |
284 | * Needed to revert to CoffeeKup 0.2.3 in order to support deploying to node < 0.4.7
285 |
286 | 0.0.6 / 2011-08-15
287 | ==================
288 |
289 | * Extracted Story, List, and Syndication examples to separate project (timbits-example)
290 | * Utilizes kitkat for testing
291 | * Added test cases
292 | * Utilizes env for port number if available
293 | * Application options object replaces parameters
294 | * Feature rich HTML5 story template in examples
295 | * Added List and Syndication widgets to examples
296 | * Incorporated connect-esi packaged
297 |
298 | 0.0.5 / 2011-08-04
299 | ==================
300 |
301 | * Now uses Pantry for JSON/XML data retrieval
302 | * Upgraded to Express 2.4.3 and CoffeeKup 0.3.0beta
303 | * Examples updated to account for the above changes
304 |
305 | 0.0.4 / 2011-07-14
306 | ==================
307 |
308 | * Updated package.json and published the package
309 |
310 | 0.0.3 / 2011-07-14
311 | ==================
312 |
313 | * Fix some typos
314 | * Started documentation
315 | * Implement default help.coffee file
316 |
317 | 0.0.2 / 2011-07-13
318 | ==================
319 |
320 | * Created sample timbits of varying complexity
321 | * Major refactoring as we develop our examples
322 | * Reworked areas towards convention over configuration
323 | * Uses the CoffeeKup view engine by default
324 | * Created Story timbit to be used as a real world prototype
325 |
326 | 0.0.1 / 2011-07-05
327 | ==================
328 |
329 | * Initial release
330 |
331 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | (The MIT License)
2 |
3 | Copyright (c) 2011 Postmedia Network Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Timbits
2 | Widget framework based on Express
3 |
4 | ## Introduction
5 |
6 | Timbits is an attempt to build an easy, and reusable widget framework on top of Express. These widgets are meant to render independent HTML snippets based on REST based JSON/XML data sources, and brought together on a page via ESI (Edge Side Includes), or our own proprietary CSI (Client Side Includes).
7 |
8 | It's primarily meant to serve internal purposes as Postmedia Network Inc, however, it is being open sourced under the MIT License. Others may find some use for what we are doing, and still others may be able to help turn this into a more generic, and useful solution via contributions.
9 |
10 | Constructive criticism is encouraged.
11 |
12 | ## Installing
13 |
14 | Just grab [node.js](http://nodejs.org/#download) and [npm](http://github.com/isaacs/npm) and you're set:
15 |
16 | npm install timbits -g
17 |
18 | Timbits is simplistic, and finds most of it's power by running on top of some very cool node libraries such as [Express](http://expressjs.com/) and [Hogan](http://twitter.github.com/hogan.js/). It also uses our [Pantry](https://github.com/Postmedia/pantry) package for efficient utilization of JSON/XML data sources.
19 |
20 | ## Changes
21 |
22 | We completed a significant rewrite of Timbits, these changes occur between version 0.6.7 and 0.7 release of timbits. For those using previous versions of Timbits here is a quick rundown of the changes.
23 |
24 | * major rewrite of Timbits source to JavaScript (formerly CoffeeScript)
25 | * generates JavaScript files by default (CoffeeScript is optional)
26 | * default view engine is now Hogan (formerly CoffeeKup)
27 | * dynamic helpers are no longer supported
28 | * sessions are no longer enabled by default
29 | * uses Winston for logging (formerly coloured-log)
30 |
31 | I'll be making a post on my [blog](http://mred9.com) providing more details on these changes, and the reasoning behind them.
32 |
33 | Please be aware that the 0.7.x series should be considered experimental, and not for production use. Once we've stabilized this rewrite it will be released as v0.8.x
34 |
35 | ### Running previous versions on 0.7
36 |
37 | Projects using timbits prior to v0.7 based on CoffeeScript, and Coffeecup require a few changes in order to run. The instructions here provide the details required to get a simple timbit functional.
38 |
39 | There are two specific modifications required, the first is to include a dependency for CoffeeScript, and Coffeecup in your timbits package.json file:
40 |
41 | "dependencies": {
42 | "coffee-script": "~1.3.3",
43 | "coffeecup": "~0.3.20",
44 | ..... additional dependencies here
45 | }
46 |
47 | The second change is to the server.js file, you need to do three things:
48 | * require coffee-script
49 | * specify the engine parameter when instantiating the server object
50 | * set the rendering method to handle coffeecup templates.
51 |
52 | The structure of the main server.js file could look like this:
53 |
54 | require('coffee-script');
55 |
56 | var timbits = require('timbits');
57 | var server = timbits.serve({ engine: 'coffee' });
58 |
59 | server.engine( 'coffee', require('coffeecup').__express );
60 |
61 | That is all it will take, unless of course you are using dynamic view helpers, for that we need to make a change to the individual timbits themselves.
62 |
63 | Let's suppose you had a helper file helpers/headlines.coffee in your timbits project, in versions prior to 0.7, timbits dynamically loaded all helpers in the 'helpers' path. These helpers were automatically loaded into the context based on the helper filename, so the example mentioned would load any helper functionality under context.headlines, now you must do this manually.
64 |
65 | Include your helpers in each timbit that requires them, at the start of the fetch method. Assuming for example the helper file is headlines.coffee, you would use:
66 |
67 | @fetch req, res, context, options, =>
68 | # Add helpers manually at beginning of fetch
69 | context.headlines = require("../helpers/headlines.coffee");
70 |
71 |
72 | ## Using
73 |
74 | The structure of a Timbits application is fairly simple. To start, you need a two lines in your main server.js file.
75 |
76 | var timbits = require('timbits');
77 | var server = timbits.serve();
78 |
79 | Timbits, like many other frameworks, prefers convention over configuration. It will look for specific folders upon startup, and if you manually create your project (vs using the Timbits command line to generate a new project) you should create the following folders.
80 |
81 | * /public - images, javascript, stylesheets etc.
82 | * /timbits - this is where we place the individual timbit (widget) files
83 | * /views - views for a particular timbit are placed in a subfolder of the same name
84 |
85 | When you start the server, it will automatically load all the timbits found in the /timbits folder. The name of the timbit is determined by the name of the file, and that name in turn determines the default route (/name/:view?), and the default view (/views/name/default.coffee). Aside from the location, and name of the timbits, the rest is customizable as shown in the examples.
86 |
87 | Timbits can be created in JavaScript (default), or CoffeeScript. This documentation will assume JavaScript.
88 |
89 | The simplest of timbits takes the following form:
90 |
91 | // Load the timbits module
92 | var timbits = require('../../lib/timbits');
93 |
94 | // create and export the timbit
95 | var timbit = module.exports = new timbits.Timbit();
96 |
97 | That's it! If you created this as /timbits/test.js, and placed a default view at /views/test/default.hjs, you could "eat" this timbit by going to /test in a browser. See the "plain" timbit for an example of this.
98 |
99 | ## How it works
100 |
101 | When you start up a Timbits based project, the framework will dynamically load each timbit from the /timbits/ folder. A timbit is
102 | nothing more than a .js, or .coffee file which creates, and exports a new instance of the Timbit class.
103 |
104 | This Timbit class has one very important method named "eat" which is the entry point for every request. This method is intended to
105 | be overwritten. When called, the framework will pass in the following three variables
106 |
107 | * req - the http request object
108 | * res - the http response object
109 | * context - a hash of data properties
110 |
111 | The req, and res parameters come straight from Express.js and are documented [here](http://expressjs.com/guide.html).
112 | The context object originates within Timbits, and will initially contain the following properties:
113 |
114 | * name - the name of the timbit being executed
115 | * view - the name of the specified view
116 | * maxAge - the number of seconds the response should be cached for
117 |
118 | It's important to note that it is possible to override the view, and/or the maxAge during the execution of the request if needed.
119 |
120 | Each timbit by default supports HTTP GET method, it is possible to support one of the alternativce standard HTTP methods as defined in [RFC 2616](http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html), specifically we support "GET", "POST", "PUT", "HEAD", and "DELETE". To specified the methods you want to allow (beyond "GET") define the methods as json properties in the timbit.methods property like this
121 |
122 | timbit.methods = { 'get': false, 'post': true }
123 |
124 | Values are boolean, the methods are not case sensitive, you only need to specify the methods you wish to override from the default settings. Any request for your timbit with a method not specified will result in an HTTP 405 "Method Not Supported" error.
125 |
126 | Each route, querystring, and post parameter is also added to the context as a property. So, if one included ?page=5 in the request url this value would be made available to you as context.page
127 |
128 | Within the eat method, the simplest of implementations will merely call the render method to, well, render the given view using the data found with the context. The render method takes the same three parameters originally passed into the eat method. Within the render method Timbits will pass the context to the view rendering engine so that those values can be
129 | referenced within the view. So a very simple eat method could look like this
130 |
131 | timbit.eat = function(req, res, context) {
132 | this.render(req, res, context);
133 | };
134 |
135 | In fact, that's exactly what it looks like within the Timbit class, so if you don't override it, the timbit will simply pass on the request parameters for rendering. But that's generally not very useful. The intent in most cases is to add additional data to the context, whether it be generated on the local server, like a timestamp (see the cherry timbit example), or more likely from an external resource like a RSS, or JSON feed/API.
136 |
137 | In most cases you'll need to pull in remote data, so we've incorporated [Pantry](https://github.com/Postmedia/pantry) into Timbits to make fetching (and caching) those resources extra easy. This is available to you via the built in fetch method.
138 |
139 | The fetch method takes five parameters. The first three are the same as the render method: req, res, context. It then adds two additional parameters
140 |
141 | * options - This is a hash of options passed directly to Pantry's fetch method. It identifies the source URI of the resource you want to retrieve along with a host of other options as documented within Pantry
142 | * callback - This is an optional parameter which identifies the function to execute once the request has been completed. It defaults to the render method
143 |
144 | The fetch method will automatically add the retrieved resource to context.data by default. If that isn't acceptable, you can also add a property called "name" to the options parameter to specify an alternate property name to store the data in. This is shown in the included "chocolate" example as follows:
145 |
146 | // specify the data source
147 | var src = {
148 | name: 'ip',
149 | uri: "http://jsonip.com?q=" + context.q + "&rpp=" + context.rpp
150 | };
151 |
152 | // use the helper method to fetch the data
153 | // timbit.fetch will call timbit.render once we have the data
154 | timbit.fetch(req, res, context, src);
155 |
156 | Be aware that if (using the example above) context.ip already exists, timbits will transform context.ip into an array (if it isn't already), and append the retrieved resource to the array.
157 |
158 | Once the request has been completed, the data added to the context, and no callback is specified, the fetch method will then call the render method for you. If you need to request some additional resources, or perhaps further manipulate the resource once it's been fetched, simply tack on the optional callback parameter, and implement your follow up code. Timbits will pass on the req, res, and context parameters to the callback. Just be sure to call the render method once you're done.
159 |
160 | timbit.fetch(req, res, context, src, function(req, res, context){
161 | // run your custom code here
162 | // ......
163 | // ......
164 |
165 | // and then call render
166 | timbit.render(req, res, context);
167 | });
168 |
169 | ## Additional Features
170 |
171 | ### Command Line
172 |
173 | Create a new project, generate a timbit (and default view), or run the project with runjs. Run timbits -h for complete list of commands, and options available.
174 |
175 | timbits n[ew] [project]
176 | timbits g[enerate] [timbit]
177 | timbits s[erver] [filename]
178 | timbits t[est]
179 |
180 | ### Running timbits
181 |
182 | There are a number of ways to run timbits, but the easiest approach during development is to utilize the timbits server command. Doing so will by default run the server.js file, although an alternate filename (server.coffee) can be given. The timbits server command has the added benefits of loading up any environment variables from an optional .env file, as well as automatically restarting the server if any files change.
183 |
184 | ### Server Configuration
185 |
186 | The server object can be configured during initialization as needed. The following is a list of possible configuration parameters and their [default value]
187 |
188 | * appName - Friendly name of the timbits application ['Timbits']
189 | * engine - Default view engine ['hjs']
190 | * base - A base path for nested servers. e.g. '/my/nested/server' ['']
191 | * port - The server port to listen on if PORT or C9_PORT env labels are not present. [5678]
192 | * home - The physical path to the root project folder [process.cwd()]
193 | * maxAge - The default cache length in seconds [60]
194 | * discovery - Enables you to serve out your library definition via /timbits/json [true]
195 | * help - Enables the automated help pages via /timbits/help and /[name]/help [true]
196 | * test - Enables the automated test pages via /timbits/test and /[name]/test
197 | * json - Enables the built in json view for timbits via /[name]/json [true]
198 | * jsonp - Enables jsonp requests to json resources
199 |
200 | The following example shows how to run a nested application using the [coffeecup](https://github.com/gradus/coffeecup) view engine in place of Hogan and disables the automated test routes.
201 |
202 | var timbits = require('timbits');
203 | var server = timbits.serve({appName: 'My Nested Timbits', engine: 'coffee', base: '/nested/app', test: false});
204 | server.engine('coffee', require('coffeecup').__express);
205 | console.log("Press Ctrl+C to Exit");
206 |
207 | ### Parameter declaration and validation
208 |
209 | For each timbit, you can define a list of parameters which are automatically validated during execution. This also powers the automated help, test, and discovery functions (see further below). Parameter attributes you can manipulate are:
210 |
211 | * alias - an alternate name for the parameter
212 | * description
213 | * type - data type expected, one of String (default), Number, Boolean, or Date
214 | * default - default value to use if value not specified
215 | * multiple - true/false (defaults to false), indicates whether multiple values are allowed
216 | * required - true/false (defaults to false), indicates whether this is a required parameter
217 | * values - an array of possible values
218 | * strict - true/false (defaults to false), indicates whether the value must be one of the defined possible values
219 |
220 | Example (from plain)
221 |
222 | timbit.params = {
223 | who: {description: 'Name of person to greet', alias: 'name', default: 'Anonymous', multiple: false, required: false, strict: false, values: ['Ed', 'World']}
224 | year: {description: 'To test multi parameters and drive Kevin crazy', type: 'Number', values: [1999, 2011]}
225 | }
226 |
227 | ### Dynamic Help
228 |
229 | Along with the list of parameters, one can also augment a timbit definition with a description (timbit.about), and a list of valid examples (timbit.examples).
230 |
231 | Example (from plain)
232 |
233 | timbit.params = {
234 | who: {
235 | description: 'Name of person to greet',
236 | "default": 'Anonymous',
237 | multiple: false,
238 | required: false,
239 | strict: false,
240 | values: ['Ed', 'World']
241 | },
242 | retro: {
243 | description: 'To test multi parameters and drive Kevin crazy',
244 | type: 'Boolean'
245 | }
246 | };
247 |
248 | Together these all help power the automated, dynamic help page for each timbit, which can be found using the hardcoded 'help' view. e.g. /plain/help
249 |
250 | There is also a built in help index that for any timbits project located at /timbits/help
251 |
252 | ### Dynamic Testing
253 |
254 | The examples, and params used to power the help pages are also used to power the automated test pages. Each timbit has a 'test' view which will utilize this information to define, and execute some basic testing of the timbit. e.g. /plain/test
255 |
256 | There is also a master test page located at /timbits/test which will execute tests for all timbits
257 |
258 | Although not overly sophisticated, it will ensure your definitions, examples, and views are valid and compile properly. It is also useful for remote monitoring of production systems.
259 |
260 | Additional functional testing can, and should be implemented via a testing library, such as [mocha](http://visionmedia.github.com/mocha/)
261 |
262 | By default, only a limited number of tests are executed, but you can run through a much larger set of tests by appending /all to the test path. This will generate a much more thorough list of test urls using every possible combination of required parameters, and their sample values, along with each possible sample value for optional parameters.
263 |
264 | e.g. /mytimbit/test/all (run through all tests for mytimbit)
265 |
266 | e.g. /timbits/test/all (run through all tests for all timbits)
267 |
268 | Note that you can provide your own list of test urls in place of the automated ones by simple overriding the generateTests() method for a given timbit, and returning a simply array of virtual paths
269 |
270 | Example:
271 |
272 | timbit.generateTests = function(alltests){
273 | var tests = [
274 | '/mytimbit/myview?name=bob',
275 | '/mytimbit/altview?name=sue'
276 | ];
277 |
278 | if (alltests)
279 | tests.push(
280 | '/mytimbit/myview?name=bob&age=39',
281 | '/mytimbit/myview?name=sue&gender=F'
282 | );
283 |
284 | return tests;
285 | };
286 |
287 | If you use the test cases provided by the timbit new project template, you can also run through these same tests (and any others you may want to add) from a terminal via the test command
288 |
289 | timbits test
290 |
291 | Running the test command will invoke the mocha test framework. The command will also load up any environment variables found in the optional .env file before starting the server, and running the tests.
292 |
293 | Just as with running tests via the browser, you can indicate you want to run through all the available tests via the --all flag. In addition, you can pass a number of options to mocha (run timbits help to see the full list). For example, to watch for file changes, and retest use the --watch flag.
294 |
295 | Example:
296 |
297 | timbits test --all --watch
298 |
299 | Or (if you want to minimize typing)
300 |
301 | timbits t -aw
302 |
303 | ### Default view
304 |
305 | By default, the name of the default view is, well, default! You do have the ability to specify something more descriptive if you so desire. Simply set the defaultView property to whatever view name you'd like to use
306 |
307 | // the name default is so booooring. use my fancy view by default instead
308 | timbit.defaultView = 'fancy';
309 |
310 | ### Sharing of views
311 |
312 | If you have two, or more timbits for which you would like to share views, simply set the viewBase property on the timbit to the name of the timbit who's views you'd like to utilize (see the Dutchie timbit as an example)
313 |
314 | // this timbit should use the views from the chocolate timbit
315 | timbit.viewBase = 'chocolate';
316 |
317 | ### Downstream Caching
318 |
319 | There is a maxAge property that can set the number of seconds client can/should cache the response for. If not set, the default value will come from the Timbits configuration, which if not set, is currently 60 seconds by default. You can also override this value on a request by request basis by setting the context.maxAge value to whatever is appropriate.
320 |
321 | // timbit should be cached for 5 minutes
322 | timbit.maxAge = 300;
323 |
324 | The maxAge value will be included in two response headers, the standard Cache-Control header as well as a special Edge-Control header (used by Akamai)
325 |
326 | ### Built in JSON view
327 |
328 | If you're interested in rendering the data elsewhere (for example client side), or you simply want to see what's available to you in the current context for development, and debugging purposes, Timbits includes a built in view named "json" that will render the context object as 'application/json'. e.g. /dutchie/json
329 |
330 | ### Dynamic View Helpers
331 |
332 | As of v0.7.0, support for dynamic view helpers has been dropped.
333 |
334 | If you need/want to run your previous timbit project on 0.7 or above, please see the [change notes](https://github.com/postmedia/timbits#changes) from above. Details are provided on how to manually require helpers.
335 |
336 | ### Advanced caching via pantry
337 |
338 | As of [Pantry](https://github.com/Postmedia/pantry) v0.3.0, you are now able to configure alternate storage caches. As of Timbits v0.5.0 one can now control the pantry configuration properties as well as substitute the default MemoryStorage caching via the exported timbits.pantry property.
339 |
340 | var timbits = require('../src/timbits')
341 | ,RedisStorage = require('pantry/lib/pantry-redis');
342 |
343 | timbits.pantry.storage = new RedisStorage(null, null, null, 'DEBUG');
344 | timbits.pantry.configure({
345 | verbosity: 'DEBUG'
346 | });
347 |
348 | var server = timbits.serve({
349 | home: __dirname
350 | });
351 |
352 | ### Client Side Rendering
353 |
354 | Not everyone will be able to utilize an ESI processor in front of their site, or more likely on their development workstation, so we've added a quick and easy way to pull in timbits client side. Timbits supports an optional callback parameter which will package the rendered view as a simple JSONP package.
355 |
356 | Example:
357 |
358 | /chocolate?q=winning&callback=done
359 |
360 | Included with Timbits is a simple client-side JS library (/javascript/timbits.csi.js) which utilizes jQuery to post-process an HTML page for tags containing the data-csi attribute, automatically tacking on the callback parameter when needed, and pulling in the results client side. We've compiled a minimized version as well (/javascript/timbits.csi.min.js)
361 |
362 | Example:
363 |
364 |
365 |
366 |
367 | By default, the CSI library runs in "replace" mode, i.e. the returned html replaces any content within the parent element, but you can control that by setting the data-mode attribute to either "prepend", or "append"
368 |
369 |
370 |
This will be replaced by the results of the include
371 |
372 |
373 |
This will appear below the results of the include
374 |
375 |
376 |
This HTML will appear above the results of the include
377 |
378 |
379 | We've also added support for the dynamic insertion of query string values into your client side includes via the {#name} syntax. For example, the following will grab the query string parameter 'term' from the host page and insert it into the source url prior to making the request.
380 |
381 |
382 |
383 | ### Responsive Rendering
384 |
385 | In an effort to assist with minimizing page sizes (for mobile clients specifically) we've introduced a 'load' event as well as media queries within our timbits.csi.js library.
386 |
387 | To run some javascript once a timbit has been loaded, simply bind to the element's load event like this:
388 |
389 |
394 |
395 | To implement media queries, add a data-media attribute to the element like this:
396 |
397 |
398 |
399 | If you want to get even fancier, you can load (not show, see below) two different views of the same timbit depending on a specific breakpoint as in this example:
400 |
401 |
402 |
403 |
404 | Here's another example which shows some of the power behind this. Say you want to show four blog posts to all users, but an additional six for larger devices. The following will keep default payload small for smartphones while expanding the available content for tablets and desktops.
405 |
406 |
409 |
410 | Any valid media query will do. In fact, we also will respond to media query changes (including orientation!) so that as you resize the browser, any csi elements that were skipped due to unmet media queries will load once the media query is valid.
411 |
412 | A couple caveats you should be aware of. First, we will load csi includes based on the media query, but we won't unload them. Secondly, since we depend on the window.matchMedia() method, this doesn't work across all browsers. Specifically Opera, and IE9 or below. On these browsers, timbits csi will ignore the media queries and load each and every include. So you should still use CSS media queries to show/hide elements as if all the content was loaded.
413 |
414 | ### Wordpress Plugin
415 |
416 | For those of you who would like to utilize Timbits within your Wordpress environment, we've created a plugin for that called [wp-timbits](https://github.com/Postmedia/wp-timbits) which provides shortcode and widget support, supports Timbits' auto-discovery feature, includes the csi rendering library (with media query support) and has the ability to turn your Wordpress instance into a Timbits server.
417 |
418 | ## Road Map
419 |
420 | We have a number of items in the pipeline which we believe will provide a lot of power to this platform, such as:
421 |
422 | * Integrated benchmarks
423 | * Real-time data updates via Socket.IO
424 |
425 |
426 | ## Created by
427 |
428 | * Edward de Groot
429 | * Keith Benedict
430 | * Donnie Marges
431 | * Stephen Veerman
432 | * Kevin Gamble
433 |
--------------------------------------------------------------------------------
/bin/template/History.md:
--------------------------------------------------------------------------------
1 | 0.0.1 / current date
2 | ====================
3 |
4 | * Created new Timbits project
5 |
--------------------------------------------------------------------------------
/bin/template/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 20XX [Some Company]
2 |
--------------------------------------------------------------------------------
/bin/template/README.md:
--------------------------------------------------------------------------------
1 | # Timbits
2 | Built using [Timbits](http://github/com/postmedia/timbits)
3 |
4 |
--------------------------------------------------------------------------------
/bin/template/server.js:
--------------------------------------------------------------------------------
1 | var timbits = require('timbits');
2 | var server = timbits.serve();
3 | console.log("Press Ctrl+C to Exit");
--------------------------------------------------------------------------------
/bin/template/server_coffee.js:
--------------------------------------------------------------------------------
1 | require('coffee-script');
2 | var timbits = require('timbits');
3 | var server = timbits.serve();
4 | console.log("Press Ctrl+C to Exit");
--------------------------------------------------------------------------------
/bin/template/test/timbits-test.coffee:
--------------------------------------------------------------------------------
1 | # reduce logging levels to provide clean test feedback
2 | process.env.TIMBITS_VERBOSITY = 'critical'
3 | process.env.PANTRY_VERBOSITY = 'critical'
4 |
5 | # Module dependencies.
6 | timbits = require 'timbits'
7 | should = require 'timbits/node_modules/should'
8 | request = require 'timbits/node_modules/request'
9 |
10 | # should http extensions
11 | require 'timbits/node_modules/should-http'
12 |
13 | port = 8785
14 | alltests = process.env.TIMBITS_TEST_WHICH is 'all'
15 |
16 | server = timbits.serve( {port: port })
17 |
18 | validateRequest = (method, vpath, expect = 'html') ->
19 | describe vpath, ->
20 | test_msg = "should respond with #{expect} and status 200"
21 | if typeof expect isnt 'string'
22 | test_msg = "should respond with status #{expect}"
23 |
24 | it test_msg, (done) ->
25 | if method is "GET"
26 | request "http://localhost:#{port}#{vpath}", (err, res) ->
27 | should.not.exist err
28 | if typeof expect is 'string'
29 | res.should.have.status 200
30 | if expect is 'json'
31 | res.should.be.json
32 | else
33 | res.should.be.html
34 | else
35 | res.should.have.status expect
36 | done()
37 | else if method is "POST"
38 | request.post "http://localhost:#{port}#{vpath}", (err, res) ->
39 | should.not.exist err
40 | if typeof expect is 'string'
41 | res.should.have.status 200
42 | if expect is 'json'
43 | res.should.be.json
44 | else
45 | res.should.be.html
46 | else
47 | res.should.have.status expect
48 | done()
49 |
50 |
51 | describe 'timbits', ->
52 |
53 | describe 'automated test cases', ->
54 | for name, timbit of timbits.box
55 | if timbit.examples? and timbit.methods["GET"]
56 | describe "specified examples for #{name}", ->
57 | for example in timbit.examples
58 | validateRequest "GET", example.href
59 |
60 | if timbit.examples? and timbit.methods["POST"]
61 | describe "specified examples for #{name}", ->
62 | for example in timbit.examples
63 | validateRequest "POST", example.href
64 |
65 | dynatests = timbit.generateTests(alltests)
66 | if dynatests.length
67 | describe "dynamic tests for #{name}", ->
68 | for href in dynatests
69 | validateRequest "GET", href if timbit.methods["GET"]
70 | validateRequest "POST", href if timbit.methods["POST"]
71 |
72 |
--------------------------------------------------------------------------------
/bin/template/test/timbits-test.js:
--------------------------------------------------------------------------------
1 | // reduce logging levels to provide clean test feedback
2 | process.env.TIMBITS_VERBOSITY = 'critical';
3 | process.env.PANTRY_VERBOSITY = 'critical';
4 |
5 | // load modules
6 | var timbits = require('../lib/timbits')
7 | , should = require('should')
8 | , path = require('path')
9 | , request = require('request')
10 |
11 | // load hhtp extensions for should
12 | require('should-http');
13 |
14 | // set testing environment
15 | var port = 8785
16 | , alltests = process.env.TIMBITS_TEST_WHICH === 'all';
17 |
18 | // create test server
19 | var server = timbits.serve({
20 | home: process.cwd(),
21 | port: port,
22 | verbosity: 'critical'
23 | });
24 |
25 |
26 | // helper function for testing requests
27 | function validateRequest(method, vpath, expect) {
28 | expect = expect || 'html';
29 |
30 | describe(vpath, function() {
31 | var test_msg = "should respond with " + expect + " and status 200";
32 | if (typeof expect !== 'string')
33 | test_msg = "should respond with status " + expect;
34 |
35 | it(test_msg, function(done) {
36 | if (method=="GET") {
37 | request("http://localhost:" + port + vpath, function(err, res) {
38 | should.not.exist(err);
39 | if (typeof expect === 'string') {
40 | res.should.have.status(200);
41 | if (expect === 'json')
42 | res.should.be.json;
43 | else
44 | res.should.be.html;
45 | } else {
46 | res.should.have.status(expect);
47 | }
48 | done();
49 | });
50 | }
51 | else if (method=="POST") {
52 | request.post("http://localhost:" + port + vpath, function(err, res) {
53 | should.not.exist(err);
54 | if (typeof expect === 'string') {
55 | res.should.have.status(200);
56 | if (expect === 'json')
57 | res.should.be.json;
58 | else
59 | res.should.be.html;
60 | } else {
61 | res.should.have.status(expect);
62 | }
63 | done();
64 | });
65 | }
66 | });
67 | });
68 | };
69 |
70 |
71 | describe('timbits', function() {
72 | describe('automated test cases', function() {
73 | for (name in timbits.box) {
74 |
75 | timbit = timbits.box[name];
76 |
77 | // GET requests
78 | if (timbit.examples != null && timbit.methods['GET']) {
79 | describe('specified examples for ' + name, function() {
80 | timbit.examples.forEach(function(example) {
81 | validateRequest('GET', example.href);
82 | });
83 | });
84 | }
85 | // Post Requests
86 | if (timbit.examples != null && timbit.methods['POST']) {
87 | describe('specified examples for ' + name, function() {
88 | timbit.examples.forEach(function(example) {
89 | validateRequest('POST', example.href);
90 | });
91 | });
92 | }
93 |
94 | var dynatests = timbit.generateTests(alltests);
95 | if (dynatests.length) {
96 | describe("dynamic tests for " + name, function() {
97 | dynatests.forEach(function(href) {
98 | if (timbit.methods['GET'])
99 | validateRequest('GET', href);
100 | if (timbit.methods['POST'])
101 | validateRequest('POST', href);
102 | });
103 | });
104 | }
105 | }
106 | });
107 | });
--------------------------------------------------------------------------------
/bin/template/timbit.coffee:
--------------------------------------------------------------------------------
1 | # Timbit
2 |
3 | # load the timbits module
4 | timbits = require 'timbits'
5 |
6 | # create and export the timbit
7 | timbit = module.exports = new timbits.Timbit()
8 |
9 | # additional timbit implementation code follows...
10 |
11 | #timbit.about = 'a description about this timbit'
12 |
13 | #timbit.examples = [
14 | # {href: '/timbit/?q=winning', caption: 'Default View'}
15 | # {href: '/timbit/alternate?q=winning', caption: 'Alternate View'}
16 | #]
17 |
18 | #timbit.params = {
19 | # q: {description: 'Keyword to search for',required: true, strict: false, values: ['Coffee', 'Timbits']}
20 | #}
21 |
22 | timbit.eat = (req, res, context) ->
23 |
24 | src = {
25 | uri: "http://jsonip.com/?q=#{context.q}"
26 | }
27 |
28 | # use the helper method to @fetch the data
29 | # @fetch will call @render once we have the data
30 | @fetch req, res, context, src
--------------------------------------------------------------------------------
/bin/template/timbit.js:
--------------------------------------------------------------------------------
1 | // Timbit
2 |
3 | // load the timbits module
4 | var timbits = require('timbits');
5 |
6 | //create and export the timbit
7 | var timbit = module.exports = new timbits.Timbit();
8 |
9 | // additional timbit implementation code follows...
10 |
11 | /*
12 | timbit.about = 'a description about this timbit';
13 |
14 | timbit.examples = [
15 | {
16 | href: '/timbit/?q=winning',
17 | caption: 'Default View'
18 | }, {
19 | href: '/timbit/alternate?q=winning',
20 | caption: 'Alternate View'
21 | }
22 | ];
23 |
24 | timbit.params = {
25 | q: {
26 | description: 'Keyword to search for',
27 | required: true,
28 | strict: false,
29 | values: ['Coffee', 'Timbits']
30 | }
31 | };
32 | */
33 |
34 | timbit.eat = function(req, res, context) {
35 | var src = {
36 | uri: 'http://jsonip.com/?q=' + context.q
37 | };
38 | timbit.fetch(req, res, context, src);
39 | };
--------------------------------------------------------------------------------
/bin/timbits:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | // load modules
4 | var fs = require('fs')
5 | , util = require('util')
6 | , path = require('path')
7 | , spawn = require("child_process").spawn
8 | , which = require(path.join(__dirname, '..', 'node_modules', 'which')).sync;
9 |
10 | // configure optimist
11 | var optimist = require(path.join(__dirname, '..', 'node_modules', 'optimist'))
12 | .alias('coffee', 'c')
13 | .describe('c', 'language: Use coffee-script files')
14 | .alias('all', 'a')
15 | .describe('a', 'test: run all dynamic tests')
16 | .alias('R', 'reporter').string('R')
17 | .describe('R', 'test: specify reporter to use')
18 | .alias('g', 'grep').string('g')
19 | .describe('g', 'test: only run tests matching ')
20 | .alias('i', 'invert')
21 | .describe('i', 'test: inverts --grep matches')
22 | .alias('t', 'timeout').default('t', 2000)
23 | .describe('t', 'test: set test-case timeout in milliseconds')
24 | .alias('s', 'slow').default('s', 75)
25 | .describe('s', 'test: "slow" test threshold in milliseconds')
26 | .alias('w', 'watch')
27 | .describe('w', 'test: watch for changes and retest')
28 | .alias('d', 'debug')
29 | .describe('d', 'test: enable node debugger')
30 | .alias('b', 'bail')
31 | .describe('b', 'test: bail after first test failure')
32 | .usage(
33 | "Timbits Command Help\n\n \
34 | timbits n[ew] [project] [-c|--coffee]\n\ \
35 | Creates new project template and subfolders\n\n \
36 | timbits g[enerate] [timbit] [-c|--coffee]\n \
37 | Scaffolds a new timbit and default view\n\n \
38 | timbits s[erver] [filename]\n \
39 | Launches runjs with [server.js]\n\n \
40 | timbits t[est] [options]\n \
41 | Run mocha test cases\n\n \
42 | timbits [-]v[ersion]\n \
43 | Displays installed timbits library version \
44 | ");
45 |
46 | // determine path to our project template files
47 | var template = path.join(__dirname, 'template');
48 |
49 | // determine path to a command in local node modules
50 | function localCmd(name) {
51 | var cmd = path.join(__dirname, '..', 'node_modules', '.bin', name);
52 | if(process.platform === 'win32') {
53 | cmd += '.cmd';
54 | }
55 | return cmd;
56 | }
57 |
58 | function cp(source, destination) {
59 | var data = fs.readFileSync(source);
60 | fs.writeFileSync(destination, data);
61 | }
62 |
63 | function loadEnv() {
64 | if (fs.existsSync('.env')) {
65 | var env_lines = fs.readFileSync('.env').toString().split('\n');
66 | for (var i = 0; i < env_lines.length; i++) {
67 | var key_value = env_lines[i].trim().split('=');
68 | if (key_value.length === 2) {
69 | console.log('Setting env var: ' + env_lines[i]);
70 | process.env[key_value[0]] = key_value[1];
71 | }
72 | }
73 | }
74 | }
75 |
76 | function newProject(name, ext) {
77 | console.log("Creating project '" + name + "'");
78 |
79 | var directories = ["timbits", "views", "test"];
80 | var files = ["History.md", "README.md", "LICENSE"];
81 |
82 | var project_home = path.join(process.cwd(), name);
83 |
84 | if(fs.existsSync(project_home)) {
85 | console.error('\033[31mERROR! ' + project_home + ' already exists!\033[0m');
86 | return;
87 | }
88 |
89 | // create parent directory
90 | console.log('Creating project directory: ' + project_home);
91 | fs.mkdirSync(project_home);
92 |
93 | // create child directories
94 | for (var i = 0; i < directories.length; i++) {
95 | console.log('Creating child directory: ' + directories[i]);
96 | fs.mkdirSync(path.join(project_home, directories[i]));
97 | }
98 |
99 | // copy template files
100 | for (var i = 0; i < files.length; i++) {
101 | console.log('Copying template file: ' + files[i]);
102 | cp(
103 | path.join(template, files[i]),
104 | path.join(project_home, files[i])
105 | );
106 | }
107 |
108 | // copy server.js file
109 | if (ext == 'js') {
110 | console.log('Copying template file: server.js');
111 | cp(
112 | path.join(template, 'server.js'),
113 | path.join(project_home, 'server.js')
114 | );
115 | } else {
116 | console.log('Copying template file: server_coffee.js -> server.js');
117 | cp(
118 | path.join(template, 'server_coffee.js'),
119 | path.join(project_home, 'server.js')
120 | );
121 | }
122 |
123 | // copy test template
124 | console.log('Generating test template: ' + name + '-test.' + ext );
125 | cp(
126 | path.join(template, 'test', 'timbits-test.' + ext),
127 | path.join(project_home, 'test', name + '-test.' + ext)
128 | );
129 |
130 | // create package file
131 | console.log('Generating package.json: package.json');
132 | var pkg;
133 | if ( ext == 'js' ) {
134 | pkg = {
135 | name: name,
136 | description: 'Widgets built using Timbits',
137 | version: '0.0.1',
138 | dependencies: {
139 | timbits: getPackage().version
140 | }
141 | };
142 | } else {
143 | pkg = {
144 | name: name,
145 | description: 'Widgets built using Timbits',
146 | version: '0.0.1',
147 | dependencies: {
148 | timbits: getPackage().version,
149 | 'coffee-script': '~1.3.3'
150 | }
151 | };
152 | }
153 | fs.writeFileSync(
154 | path.join(project_home, 'package.json'),
155 | JSON.stringify(pkg)
156 | );
157 |
158 | // prompt user to configure npm package
159 | var npm_init = spawn(
160 | which('npm'),
161 | ['init'],
162 | {
163 | cwd: project_home,
164 | env: process.env,
165 | stdio: 'inherit'
166 | }
167 | );
168 |
169 | npm_init.on('exit', function(code) {
170 | if(code === 0) {
171 | // install dependencies
172 | spawn(
173 | which('npm'),
174 | ['install', '-d'],
175 | {
176 | cwd: project_home,
177 | env: process.env,
178 | stdio: 'inherit'
179 | }
180 | );
181 | }
182 |
183 | console.log('Finished!');
184 | });
185 |
186 | }
187 |
188 | function generateTimbit(name, ext) {
189 | console.log("Generating timbit '" + name + "'");
190 | if(
191 | fs.existsSync(path.join('timbits', name + '.' + ext)) ||
192 | fs.existsSync(path.join('views', name))
193 | ) {
194 | console.error('\033[31mERROR! The timbit ' + name + ' may already exist!\033[0m');
195 | return;
196 | }
197 |
198 | if (
199 | ! fs.existsSync(path.join('timbits')) ||
200 | ! fs.existsSync(path.join('views'))
201 | ) {
202 | console.error('\033[31mERROR! Current folder does not appear to be timbits project!\033[0m');
203 | return;
204 | }
205 |
206 | // create timbit
207 | console.log('Creating timbits/' + name + '.' + ext);
208 | cp(
209 | path.join(template, 'timbit.' + ext),
210 | path.join('timbits', name + '.' + ext)
211 | );
212 |
213 | // create default view
214 | console.log('Creating views/' + name + '/default.hjs');
215 | fs.mkdirSync(
216 | path.join('views', name)
217 | );
218 |
219 | fs.writeFileSync(
220 | path.join('views', name, 'default.hjs'),
221 | "Default {{name}} Timbit View
"
222 | );
223 |
224 | }
225 |
226 |
227 |
228 | function startServer(filename) {
229 | console.log("Starting server (" + filename + ")");
230 | loadEnv();
231 |
232 | spawn(localCmd('runjs'), [filename], {
233 | stdio: [null, process.stdout, process.stderr],
234 | env: process.env
235 | });
236 | }
237 |
238 | function runTests(alltests, options) {
239 | if (alltests) {
240 | process.env.TIMBITS_TEST_WHICH = 'all';
241 | }
242 | loadEnv();
243 |
244 | var mocha = spawn(localCmd('mocha'), options, {
245 | stdio: [null, process.stdout, process.stderr],
246 | env: process.env
247 | });
248 | }
249 |
250 | function getPackage() {
251 | var data = fs.readFileSync(path.join(__dirname, '..', 'package.json'));
252 | return JSON.parse(data);
253 | }
254 |
255 | function showVersion() {
256 | var pkg = getPackage();
257 | console.log(pkg.version);
258 | }
259 |
260 | function showHelp() {
261 | console.log(optimist.help());
262 | }
263 |
264 | var argv = optimist.argv;
265 | var ext;
266 | switch (argv._[0]) {
267 | case 'n':
268 | case 'new':
269 | if (argv._.length < 2) {
270 | console.error('\033[31mnew project requires a name\033[0m');
271 | showHelp();
272 | } else {
273 | if ( argv['c'] || argv['coffee'] ) {
274 | ext = 'coffee';
275 | } else {
276 | ext = 'js';
277 | }
278 | newProject(argv._[1], ext);
279 | }
280 | break;
281 |
282 | case 'g':
283 | case 'generate':
284 | if (argv._.length < 2) {
285 | console.error('\033[31mnew timbit requires a name\033[0m');
286 | showHelp();
287 | } else {
288 | if ( argv['c'] || argv['coffee'] ) {
289 | ext = 'coffee';
290 | } else {
291 | ext = 'js';
292 | }
293 | generateTimbit(argv._[1], ext);
294 | }
295 | break;
296 |
297 | case 's':
298 | case 'server':
299 | filename = argv._.length > 1 ? optimist.argv._[1] : 'server.js';
300 | startServer(filename);
301 | break;
302 |
303 | case 't':
304 | case 'test':
305 | options = ['--growl', '--colors'];
306 | for (key in argv) {
307 | switch(key){
308 | case 'reporter':
309 | case 'grep':
310 | case 'timeout':
311 | case 'slow':
312 | options.push('--' + key, argv[key]);
313 | break;
314 | case 'invert':
315 | case 'watch':
316 | case 'debug':
317 | case 'bail':
318 | options.push('--' + key)
319 | break;
320 | case 'coffee':
321 | options.push( '--compilers', 'coffee:coffee-script' );
322 | break;
323 | }
324 | }
325 | runTests(argv.a, options);
326 | break;
327 |
328 | case 'v':
329 | case 'version':
330 | showVersion();
331 | break;
332 |
333 | default:
334 | if (argv.v != null) {
335 | showVersion();
336 | } else {
337 | showHelp();
338 | }
339 | }
--------------------------------------------------------------------------------
/examples/helpers/hello.js:
--------------------------------------------------------------------------------
1 | module.exports = function hello(name) {
2 | return 'Hello there ' + name;
3 | };
--------------------------------------------------------------------------------
/examples/server-redis.coffee:
--------------------------------------------------------------------------------
1 | # Module dependencies.
2 | timbits = require '../src/timbits'
3 |
4 | # use redis for caching
5 | RedisStorage = require 'pantry/lib/pantry-redis'
6 | timbits.pantry.storage = new RedisStorage(null, null, null, 'DEBUG')
7 | timbits.pantry.configure {verbosity: 'DEBUG'}
8 |
9 | # start serving timbits
10 | server = timbits.serve {home: __dirname}
11 |
--------------------------------------------------------------------------------
/examples/server.js:
--------------------------------------------------------------------------------
1 | //process.env.TIMBITS_VERBOSITY = 'debug';
2 | //process.env.PANTRY_VERBOSITY = 'debug';
3 |
4 | var timbits = require('../lib/timbits');
5 | var server = timbits.serve({
6 | home: __dirname
7 | });
--------------------------------------------------------------------------------
/examples/timbits/cherry.js:
--------------------------------------------------------------------------------
1 | // Cherry Timbit
2 |
3 | // load the timbits module
4 | var timbits = require('../../lib/timbits');
5 |
6 | // create and export the timbit
7 | var timbit = module.exports = new timbits.Timbit();
8 |
9 | // additional timbit implementation code follows...
10 | timbit.about = '\
11 | Example of a timbit which actually does something.\
12 | This timbit will display the current server time.\
13 | ';
14 |
15 | timbit.examples = [
16 | {
17 | href: '/cherry/',
18 | caption: 'Current Time'
19 | }
20 | ];
21 |
22 | timbit.eat = function(req, res, context) {
23 | context.now = new Date();
24 | return this.render(req, res, context);
25 | };
--------------------------------------------------------------------------------
/examples/timbits/chocolate.js:
--------------------------------------------------------------------------------
1 | // Chocolate Timbit
2 |
3 | // load the timbits module
4 | var timbits = require('../../lib/timbits');
5 |
6 | // create and export the timbit
7 | var timbit = module.exports = new timbits.Timbit();
8 |
9 | // additional timbit implementation code follows...
10 | timbit.about = '\
11 | Example of a timbit which transforms remote data.\
12 | This timbit will query Wordpress API and display the results\
13 | There are two views available, the default and the alternate\
14 | ';
15 |
16 | timbit.examples = [
17 | {
18 | href: '/chocolate/?site=sports.nationalpost.com',
19 | caption: 'Latest Sports news from The National Post'
20 | }, {
21 | href: '/chocolate/alternate-view?site=sports.nationalpost.com&tag=Hockey&number=5',
22 | caption: 'Latest five posts on Hockey from The National Post'
23 | }
24 | ];
25 |
26 | timbit.params = {
27 | site: {
28 | description: 'The Wordpress site to query',
29 | required: true,
30 | strict: false,
31 | values: ['sports.nationalpost.com', 'o.canada.com']
32 | },
33 | tag: {
34 | description: 'Tag to filter by',
35 | required: false,
36 | strict: false,
37 | values: ['Hockey', 'Detroit']
38 | },
39 | "number": {
40 | description: 'The number of posts to display',
41 | alias: 'rpp',
42 | "default": 10,
43 | strict: false,
44 | values: [3, 5, 10]
45 | }
46 | };
47 |
48 | timbit.eat = function(req, res, context) {
49 |
50 | // specify the data source
51 | var src = {
52 | name: 'wordpress',
53 | uri: "http://public-api.wordpress.com/rest/v1/sites/" + context.site + "/posts?number=" + context.number
54 | };
55 |
56 | if (context.tag) {
57 | src.uri += "&tag=" + context.tag;
58 | }
59 |
60 | // use the helper method to fetch the data
61 | // timbit.fetch will call timbit.render once we have the data
62 | timbit.fetch(req, res, context, src);
63 | };
--------------------------------------------------------------------------------
/examples/timbits/dutchie.js:
--------------------------------------------------------------------------------
1 | // Dutchie Timbit
2 |
3 | // load the timbits module
4 | var timbits = require('../../lib/timbits');
5 |
6 | // create and export the timbit
7 | var timbit = module.exports = new timbits.Timbit();
8 |
9 | // additional timbit implementation code follows...
10 | timbit.about = '\
11 | Example of a timbit which re-uses another timbits views\
12 | This timbit will query Wordpress API and display the results\
13 | This timbit re-uses the views from the chocolate timbit\
14 | ';
15 |
16 | timbit.examples = [
17 | {
18 | href: '/dutchie/?site=news.nationalpost.com',
19 | caption: 'Latest news from The National Post'
20 | }, {
21 | href: '/dutchie/alternate-view?site=news.nationalpost.com&tag=Apple&number=5',
22 | caption: 'Latest five news posts on Apple from The National Post'
23 | }
24 | ];
25 |
26 | timbit.params = {
27 | site: {
28 | description: 'The Wordpress site to query',
29 | required: true,
30 | strict: false,
31 | values: ['news.nationalpost.com', 'o.canada.com']
32 | },
33 | tag: {
34 | description: 'Tag to filter by',
35 | required: false,
36 | strict: false,
37 | values: ['Apple', 'Canada']
38 | },
39 | "number": {
40 | description: 'The number of posts to display',
41 | alias: 'rpp',
42 | "default": 10,
43 | strict: false,
44 | values: [3, 5, 10]
45 | }
46 | };
47 |
48 | // let's just re-use the chocolate timbit views and use additional cache time
49 | timbit.viewBase = 'chocolate';
50 | timbit.maxAge = 300;
51 |
52 | timbit.eat = function(req, res, context) {
53 |
54 | // specify the data source
55 | var src = {
56 | uri: "http://public-api.wordpress.com/rest/v1/sites/" + context.site + "/posts?number=" + context.number
57 | };
58 |
59 | if (context.tag) {
60 | src.uri += "&tag=" + context.tag;
61 | }
62 |
63 | // instead of using the fetch helper method, let's show how to use pantry directly
64 | timbits.pantry.fetch(src, function(error, results) {
65 | context.wordpress = results;
66 | timbit.render(req, res, context);
67 | });
68 | };
--------------------------------------------------------------------------------
/examples/timbits/plain.js:
--------------------------------------------------------------------------------
1 | // Plain Timbit
2 |
3 | // Load the timbits module
4 | var timbits = require('../../lib/timbits');
5 |
6 | // create and export the timbit
7 | var timbit = module.exports = new timbits.Timbit();
8 |
9 |
10 | // additional timbit implementation code follows...
11 | timbit.about = '\
12 | Example of the simplest timbit that could possibly be created.\
13 | This timbit will simply render a view using data from the query string.\
14 | ';
15 |
16 | timbit.examples = [
17 | {
18 | href: '/plain/',
19 | caption: 'Anonymous'
20 | }, {
21 | href: '/plain/?who=world',
22 | caption: 'Hello World'
23 | }, {
24 | href: '/plain/?who=Kevin&retro=true',
25 | caption: 'Flashback'
26 | }
27 | ];
28 |
29 | timbit.params = {
30 | who: {
31 | description: 'Name of person to greet',
32 | "default": 'Anonymous',
33 | multiple: false,
34 | required: false,
35 | strict: false,
36 | values: ['Ed', 'World']
37 | },
38 | retro: {
39 | description: 'To test multi parameters and drive Kevin crazy',
40 | type: 'Boolean'
41 | }
42 | };
--------------------------------------------------------------------------------
/examples/timbits/post.js:
--------------------------------------------------------------------------------
1 | // Timbit
2 |
3 | // load the timbits module
4 | var timbits = require('../../lib/timbits');
5 |
6 | //create and export the timbit
7 | var timbit = module.exports = new timbits.Timbit();
8 |
9 | // additional timbit implementation code follows...
10 |
11 | timbit.about = 'An example timbit that handles just the post event';
12 |
13 | // Set allowed methods, by default only GET is ever allowed
14 | timbit.methods = { 'GET': false, 'POST': true };
15 |
16 | timbit.examples = [
17 | {
18 | href: '/post',
19 | caption: 'Default View'
20 | }
21 | ];
22 |
23 |
24 | timbit.eat = function(req, res, context) {
25 | var body = '';
26 | req.on( 'data', function( data ) {
27 | body += data;
28 | } );
29 |
30 | req.on( 'end', function () {
31 | context.body = body;
32 | timbit.render( req, res, context )
33 | });
34 | };
--------------------------------------------------------------------------------
/examples/views/cherry/default.hjs:
--------------------------------------------------------------------------------
1 | The current server date/time is {{now}}
--------------------------------------------------------------------------------
/examples/views/chocolate/alternate-view.hjs:
--------------------------------------------------------------------------------
1 | Alternate {{name}} Timbit View
2 | Latest posts from '{{site}}'
3 |
4 |
5 | {{#wordpress.posts}}
6 | - {{title}} - By {{author.name}}
7 | {{/wordpress.posts}}
8 |
--------------------------------------------------------------------------------
/examples/views/chocolate/default.hjs:
--------------------------------------------------------------------------------
1 | Default {{name}} Timbit View
2 | Latest posts from '{{site}}'
3 |
4 |
5 | {{#wordpress.posts}}
6 | - {{title}}
7 | {{/wordpress.posts}}
8 |
--------------------------------------------------------------------------------
/examples/views/plain/default.hjs:
--------------------------------------------------------------------------------
1 |
2 | {{#retro}}Wazzzzzzup{{/retro}}
3 | {{^retro}}Hello{{/retro}}
4 | {{who}}
5 |
6 | {{#retro}}
7 |
15 | {{/retro}}
--------------------------------------------------------------------------------
/examples/views/post/default.hjs:
--------------------------------------------------------------------------------
1 | HTTP Post Timbit Example
2 | Send a post request with content in the body, the content is shown below the line
3 |
4 | {{body}}
5 |
--------------------------------------------------------------------------------
/lib/templates/help.hjs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{title}}
5 |
46 |
47 |
48 |
49 |
50 |
{{title}}
51 |
52 | {{#timbits}}
53 | - {{.}} »
54 | {{/timbits}}
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/lib/templates/test.hjs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{title}}
5 |
6 |
33 |
34 |
35 |
36 |
{{title}}
37 |
38 | {{#failed}}
39 |
40 |

41 |
Failed {{failed}} of {{results.length}}
42 |
43 |
44 |
45 | Timbit |
46 | URL |
47 | HTTP Status |
48 | Error Message |
49 |
50 |
51 |
52 | {{#results}}
53 | {{#failed}}
54 |
55 | {{timbit}} |
56 | {{href}} |
57 | {{status}} |
58 | {{error}} |
59 |
60 | {{/failed}}
61 | {{/results}}
62 |
63 |
64 |
65 | {{/failed}}
66 |
67 | {{#passed}}
68 |
69 |

70 |
Passed {{passed}} of {{results.length}}
71 |
72 |
73 |
74 | Timbit |
75 | URL |
76 |
77 |
78 |
79 | {{#results}}
80 | {{#passed}}
81 |
82 | {{timbit}} |
83 | {{href}} |
84 |
85 | {{/passed}}
86 | {{/results}}
87 |
88 |
89 |
90 | {{/passed}}
91 |
92 |
93 |
94 |
--------------------------------------------------------------------------------
/lib/templates/timbit-help.hjs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{title}}
5 |
52 |
53 |
54 |
55 |
56 |
57 |
{{name}}
58 |
{{about}}
59 |
60 |
Examples:
61 | {{^examples}}
62 |
Developer was too lazy to define any examples.
63 | {{/examples}}
64 |
65 | {{#examples}}
66 | - {{caption}}
67 | {{/examples}}
68 |
69 |
70 |
Views
71 |
72 | {{#views}}
73 | - {{.}}
74 | {{/views}}
75 |
76 |
77 |
Parameters
78 |
79 |
80 |
81 | Name |
82 | Alias |
83 | Description |
84 | Type |
85 | Required |
86 | Multiple |
87 | Default |
88 | Values |
89 |
90 | {{#paramsAsArray}}
91 |
92 | {{key}} |
93 | {{value.alias}} |
94 | {{value.description}} |
95 | {{value.type}}{{^value.type}}String{{/value.type}} |
96 | {{value.required}} |
97 | {{value.multiple}} |
98 | {{value.default}} |
99 |
100 | {{#value.strict}}One of:{{/value.strict}}
101 | {{^value.strict}}Examples:{{/value.strict}}
102 |
103 | {{#value.values}}
104 | - {{.}}
105 | {{/value.values}}
106 |
107 | |
108 |
109 | {{/paramsAsArray}}
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
--------------------------------------------------------------------------------
/lib/timbits.js:
--------------------------------------------------------------------------------
1 |
2 | // load required modules
3 | var fs = require('fs')
4 | , path = require('path')
5 | , querystring = require('querystring')
6 | , url = require('url')
7 | , winston = require('winston')
8 | , express = require('express')
9 | , hogan = require('hogan.js')
10 | , request = require('request');
11 |
12 | // default configuration
13 | var config = {
14 | appName: 'Timbits',
15 | base: '',
16 | port: 5678,
17 | home: process.cwd(),
18 | maxAge: 60, // default widget output cache time
19 | engine: 'hjs', // default view engine
20 | discovery: true, // support automatic discovery via /timbits/json
21 | help: true, // allow automatic help pages at /timbits/help and /[name]/help
22 | test: true, // allow automatic test pages at /timbits/test and /[name]/test
23 | json: true, // allow built in json view at /[name]/json
24 | jsonp: true // allow jsonp calls via /[name]/json?callback=
25 | };
26 |
27 | // retrieve list of matching files in a folder
28 | function filteredFiles(folder, pattern) {
29 | var files = [];
30 |
31 | if (fs.existsSync(folder)){
32 | fs.readdirSync(folder).forEach(function(file) {
33 | if (file.match(pattern) != null)
34 | files.push(file);
35 | });
36 | }
37 | return files;
38 | }
39 |
40 | // automagically load timbits found in the ./timbits folder
41 | function loadTimbits(callback) {
42 |
43 | var folder = path.join(config.home, "/timbits");
44 | var files = filteredFiles(folder, /\.(coffee|js)$/);
45 | var pending = files.length;
46 |
47 | files.forEach(function(file) {
48 | var name = file.substring(0, file.lastIndexOf("."));
49 | timbits.add(name, require(path.join(folder, file)), function() {
50 | pending--;
51 | if (pending === 0) callback();
52 | });
53 | });
54 | }
55 |
56 | // automagically load views for a given timbit
57 | function loadViews(timbit) {
58 | timbit.views = [];
59 |
60 | var pattern = new RegExp('\.' + timbits.app.settings['view engine'] + '$')
61 | , folder = path.join(config.home, 'views', timbit.viewBase);
62 |
63 | if (fs.existsSync(folder)) {
64 | var files = fs.readdirSync(folder);
65 | files.forEach(function(file) {
66 | timbit.views.push(file.replace(pattern, ''));
67 | });
68 | }
69 |
70 | // We will attempt the default view anyway and hope the timbit knows what it is doing.
71 | if (timbit.views.length === 0) timbit.views.push(timbit.defaultView);
72 | }
73 |
74 | // return a list of possible test values
75 | function getTestValues(values, alltests) {
76 | if (values != null && values.length != null && values.length !== 0) {
77 | if (alltests)
78 | return values;
79 | else
80 | return values.slice(0, 1);
81 | } else {
82 | return [];
83 | }
84 | }
85 |
86 | // compile built in templates
87 | function compileTemplate(name) {
88 | var filename = path.join(__dirname, "templates", name + '.hjs');
89 | var contents = fs.readFileSync(filename);
90 | return hogan.compile(contents.toString());
91 | }
92 |
93 | // generates the allowed methods
94 | function allowedMethods(methods) {
95 | // default values
96 | var methodsAllowed = { 'GET': true, 'POST': false, 'PUT': false, 'HEAD': false, 'DELETE': false };
97 | // check and override if one of the default methods
98 | for (var key in methods) {
99 | var newKey = key.toUpperCase();
100 | if ( methodsAllowed[newKey]!=undefined) {
101 | methodsAllowed[newKey] = Boolean(methods[key]);
102 | }
103 | }
104 | return methodsAllowed;
105 | }
106 |
107 | var timbits = this;
108 | this.box = {};
109 | this.pantry = require('pantry');
110 | this.templates = {
111 | help: compileTemplate('help'),
112 | timbitHelp: compileTemplate('timbit-help'),
113 | test: compileTemplate('test'),
114 | };
115 |
116 | this.getLogLevel = function() {
117 | switch (process.env.NODE_ENV) {
118 | case 'production':
119 | return 'info';
120 | case 'test':
121 | return 'error';
122 | default:
123 | return 'silly';
124 | }
125 | };
126 |
127 | //added winston object for logging.
128 | this.log = new (winston.Logger)({
129 | transports: [
130 | new (winston.transports.Console)({
131 | colorize: true,
132 | timestamp: true,
133 | level: this.getLogLevel()
134 | })
135 | ]
136 | });
137 |
138 | // creates, configures, and returns a standard express app
139 | this.serve = function(options) {
140 |
141 | /* configure options */
142 | for (var key in options) {
143 | value = options[key];
144 | config[key] = value;
145 | }
146 |
147 | /* configure express app */
148 | var app = timbits.app = express();
149 |
150 | app.set('views', "" + config.home + "/views");
151 | app.set('view engine', config.engine);
152 | app.set('jsonp callback', config.jsonp);
153 |
154 | app.use(express.favicon());
155 |
156 | // disable request logging for tests (avoid console clutter)
157 | if (app.get('env') === 'development')
158 | app.use(express.logger('dev'));
159 |
160 | app.use(express.compress());
161 | app.use(express.bodyParser());
162 | app.use(express.cookieParser());
163 | app.use(express.static(path.join(config.home, "public")));
164 | app.use(express.static(path.join(__dirname, "../resources")));
165 | app.use(express.errorHandler());
166 |
167 | // redirect root to help page
168 | if (config.help) {
169 | app.all(config.base + "/", function(req, res) {
170 | res.redirect(config.base + "/timbits/help");
171 | });
172 | }
173 |
174 | // route json discovery
175 | app.get(config.base + "/timbits/json", function(req, res) {
176 | if (config.discovery) {
177 | res.json(timbits.box);
178 | } else {
179 | res.send(404, "Automatic Discovery has been disabled");
180 | }
181 | });
182 |
183 | // route help page
184 | app.get(config.base + "/timbits/help", function(req, res) {
185 | if (config.help) {
186 | var context = {title: 'Timbits Help', timbits: []};
187 | for(var key in timbits.box) {
188 | context.timbits.push(key);
189 | }
190 | res.send(timbits.templates.help.render(context));
191 | } else {
192 | res.send(404, "Automatic Help has been disabled");
193 | }
194 | });
195 |
196 | // route master test page
197 | app.get(config.base + '/timbits/test/:which?', function(req, res) {
198 | if (config.test) {
199 | var alltests = (req.params.which === 'all')
200 | , all_results = []
201 | , pending = Object.keys(timbits.box).length;
202 |
203 | if (pending) {
204 | for (name in timbits.box) {
205 | var timbit = timbits.box[name];
206 | timbit.test('http://' + req.headers.host, alltests, function(results) {
207 | results.forEach(function(result) {
208 | all_results.push(result);
209 | });
210 | if (--pending === 0) {
211 | var passed = 0, failed = 0;
212 | all_results.forEach(function(result) {
213 | if (result.passed) passed++; else failed++;
214 | });
215 | res.send(timbits.templates.test.render({
216 | title: 'Testing Summary: all timbits',
217 | passed: passed,
218 | failed: failed,
219 | results: all_results
220 | }));
221 | }
222 | });
223 | }
224 | } else {
225 | res.send(ck.render(views.test, {}));
226 | }
227 | } else {
228 | res.send(404, "Automatic Test has been disabled");
229 | }
230 | });
231 |
232 | // automagically load timbits
233 | loadTimbits(function() {
234 | try {
235 | timbits.server = app.listen(process.env.PORT || process.env.C9_PORT || config.port);
236 | timbits.log.info("Timbits server listening on port " + timbits.server.address().port + " in " + app.settings.env + " mode");
237 | } catch (err) {
238 | timbits.log.error("Server could not start on port " + (process.env.PORT || process.env.C9_PORT || config.port) + ". (" + err + ")");
239 | console.log("\nPress Ctrl+C to Exit");
240 | process.exit(1);
241 | }
242 | });
243 |
244 | return app;
245 | };
246 |
247 | // use the 'add' method to place a timbit in the box
248 | this.add = function(name, timbit, callback) {
249 | timbits.log.info("Placing " + name + " in the box");
250 | timbits.box[name] = timbit;
251 |
252 | timbit.name = name;
253 | if (timbit.viewBase == null) timbit.viewBase = name;
254 | if (timbit.defaultView == null) timbit.defaultView = 'default';
255 | if (timbit.maxAge == null) timbit.maxAge = config.maxAge;
256 | timbit.methods = allowedMethods( (timbit.methods == null) ? {} : timbit.methods );
257 |
258 | loadViews(timbit);
259 |
260 | // route timbit help
261 | timbits.app.get(config.base + "/" + name + "/help", function(req, res) {
262 | if (config.help)
263 | res.send(timbits.templates.timbitHelp.render(timbit));
264 | else
265 | res.send(404, "Automatic Help has been disabled");
266 | });
267 |
268 | // route timbit testing
269 | timbits.app.get(config.base + "/" + name + "/test/:which?", function(req, res) {
270 | var alltests;
271 | if (config.test) {
272 | alltests = req.params.which === 'all';
273 | timbit.test("http://" + req.headers.host, alltests, function(results) {
274 | var passed = 0, failed = 0;
275 | results.forEach(function(result) {
276 | if (result.passed) passed++; else failed++;
277 | });
278 | res.send(timbits.templates.test.render({
279 | title: 'Testing Summary: ' + timbit.name,
280 | passed: passed,
281 | failed: failed,
282 | results: results
283 | }));
284 | });
285 | } else {
286 | res.send(404, "Automatic Test has been disabled");
287 | }
288 | });
289 |
290 | // main timbit route
291 | timbits.app.all(config.base + '/' + name + '/:view?', function(req, res) {
292 |
293 | // test if the method used is in the allowed methods,
294 | if ( timbit.methods[req.method] == undefined || timbit.methods[req.method] == false ) {
295 | res.send(405, "Method Not Allowed");
296 | return;
297 | }
298 |
299 | // set view name to default view if not specified
300 | if (req.params.view == null) req.params.view = timbit.defaultView;
301 |
302 | // initialize current request context
303 | var context = {
304 | name: timbit.name,
305 | view: timbit.viewBase + '/' + req.params.view,
306 | maxAge: timbit.maxAge
307 | };
308 |
309 | // add query string parameters to context
310 | for (var key in req.query) {
311 | var has_alias = false;
312 |
313 | if (context[key] == null && req.query[key] != null && req.query[key] !== '') {
314 |
315 | // handle aliased parameters
316 | for (var p in timbit.params) {
317 | if (timbit.params[p] === key) {
318 | has_alias = true;
319 | context[p] = req.query[key];
320 | }
321 | }
322 |
323 | // no alias found
324 | if (!has_alias)
325 | context[key] = req.query[key];
326 | }
327 | }
328 |
329 | // validate request
330 | for (var key in timbit.params) {
331 | var param = timbit.params[key];
332 |
333 | // if parameter isn't specified, use default value
334 | if (context[key] == null) context[key] = param.default;
335 |
336 | // test provided value based on type
337 | var value = context[key];
338 | if (value != null) {
339 |
340 | // default parameter type is String
341 | if (param.type == null) param.type = 'String';
342 |
343 | switch (param.type.toLowerCase()) {
344 |
345 | case 'number':
346 | context[key] = Number(value);
347 | if (isNaN(context[key]))
348 | throw value + ' is not a valid Number for ' + key;
349 | break;
350 |
351 | case 'boolean':
352 | switch (value.toLowerCase()) {
353 | case 'true':
354 | context[key] = true;
355 | break;
356 | case 'false':
357 | context[key]= false;
358 | break;
359 | default:
360 | throw value + ' is not a valid value for ' + key + '. Must be true of false';
361 | }
362 | break;
363 |
364 | case 'date':
365 | context[key] = Date.parse(value);
366 | if (isNaN(context[key]))
367 | throw value + ' is not a valid date for ' + key;
368 | break;
369 |
370 | }
371 | }
372 |
373 | if (param.required && value == null) {
374 | throw key + " is a required parameter";
375 | }
376 | if (value != null && param.strict && param.values.indexOf(value) === -1) {
377 | throw value + " is not a valid value for " + key + ". Must be one of [" + (param.values.join()) + "]";
378 | }
379 | if (value instanceof Array && !param.multiple) {
380 | throw key + " must be a single value";
381 | }
382 |
383 | }
384 |
385 | // with context created, it's time to consume this timbit
386 | timbit.eat(req, res, context);
387 | });
388 |
389 | // update example urls if base vpath specified
390 | if (config.base != null && timbit.examples != null) {
391 | timbit.examples.forEach(function(example) {
392 | example.href = config.base + example.href;
393 | });
394 | }
395 |
396 | // callback after timbit has been loaded
397 | callback();
398 |
399 | };
400 |
401 | // prototype for Timbit
402 | var Timbit = this.Timbit = function() {};
403 |
404 | Timbit.prototype.render = function(req, res, context) {
405 |
406 | // add caching headers
407 | res.setHeader("Cache-Control", "max-age=" + context.maxAge);
408 | res.setHeader("Edge-Control", "!no-store, max-age=" + context.maxAge);
409 |
410 | if (/^(\w+|(\w+-)+\w+)\/json$/.test(context.view)) {
411 | if (config.json)
412 | res.json(context);
413 | else
414 | res.send(404, "JSON view has been disabled");
415 | } else {
416 | res.render(context.view, context, function(err, str) {
417 | if (err) {
418 | timbits.log.error("Error rendering view " + context.view);
419 | req.next(err);
420 | } else {
421 | if (context.callback != null)
422 | res.json(str);
423 | else
424 | res.send(str);
425 | }
426 | });
427 | }
428 | };
429 |
430 | Timbit.prototype.fetch = function(req, res, context, options, callback) {
431 |
432 | // use built in render method by default
433 | if (callback == null) callback = this.render;
434 |
435 | var name = options.name || 'data';
436 | timbits.pantry.fetch(options, function(error, results) {
437 | if (error) {
438 | timbits.log.error("Error fetching resource '" + options.uri);
439 | req.next(error);
440 | } else {
441 | if (context[name] != null) {
442 | if (Object.prototype.toString.call(context[name][0]) === "[object Array]")
443 | context[name].push(results);
444 | else
445 | context[name] = [context[name], results];
446 | } else {
447 | context[name] = results;
448 | }
449 | callback(req, res, context);
450 | }
451 | });
452 | };
453 |
454 | Timbit.prototype.eat = function(req, res, context) {
455 | this.render(req, res, context);
456 | };
457 |
458 |
459 | Timbit.prototype.generateTests = function(alltests) {
460 |
461 | // create combination of required parameters
462 | var required = [];
463 | for (var name in this.params) {
464 | var param = this.params[name];
465 | if (param.required) {
466 | var temp = [];
467 | getTestValues(param.values, alltests).forEach(function(value) {
468 | if (required.length === 0)
469 | temp.push(name + "=" + value);
470 | else
471 | required.forEach(function(item) {
472 | temp.push(item + "&" + name + "=" + value);
473 | });
474 | });
475 | required = temp;
476 | }
477 | }
478 |
479 | // create list of possible queries using required and optional parameters
480 | var queries = [];
481 | required.forEach(function(item) {
482 | queries.push(item)
483 | });
484 |
485 | // only include optional parameters if all tests are requested
486 | if (alltests) {
487 | for (var name in this.params) {
488 | var param = this.params[name];
489 | if (!param.required) {
490 | getTestValues(param.values, alltests).forEach(function(value) {
491 | if (required.length === 0)
492 | queries.push(name + "=" + value);
493 | else
494 | required.forEach(function(item) {
495 | queries.push(item + "&" + name + "=" + value);
496 | });
497 | });
498 | }
499 | }
500 | }
501 |
502 | //create list of testable paths using available views and quiries
503 | var hrefs = [];
504 | var name = this.name
505 | this.views.forEach(function(view) {
506 | if (queries.length)
507 | queries.forEach(function(query) {
508 | hrefs.push("/" + name + "/" + view + "?" + query);
509 | });
510 | else
511 | hrefs.push("/" + name + "/" + view);
512 | });
513 |
514 | return hrefs;
515 | };
516 |
517 | Timbit.prototype.test = function(host, alltests, callback) {
518 |
519 | // generate dynamic list of test urls
520 | var tests = this.generateTests(alltests);
521 |
522 | // multiplier for tests total based on allowed methods, determine which methods are used
523 | var testsLength = 1;
524 | var getMethod = this.methods["GET"];
525 | var postMethod = this.methods["POST"];
526 | if (getMethod && postMethod)
527 | testsLength = 2;
528 |
529 | // add examples to list of tests
530 | if (this.examples) {
531 | this.examples.forEach(function(example) {
532 | tests.push(example.href);
533 | });
534 | }
535 |
536 | // run each test
537 | var results = [];
538 | var name = this.name;
539 |
540 | tests.forEach(function(href) {
541 | if (getMethod) {
542 | request(host + href, function(error, response, body) {
543 | error = error || (response.statusCode === 200 ? '' : body);
544 | results.push({
545 | timbit: name,
546 | href: href,
547 | error: error,
548 | status: response.statusCode,
549 | passed: response.statusCode === 200,
550 | failed: response.statusCode !== 200
551 | });
552 | if (results.length === (tests.length * testsLength))
553 | return callback(results);
554 | });
555 | }
556 | if (postMethod) {
557 | request.post(host + href, function(error, response, body) {
558 | error = error || (response.statusCode === 200 ? '' : body);
559 | results.push({
560 | timbit: name,
561 | href: href,
562 | error: error,
563 | status: response.statusCode,
564 | passed: response.statusCode === 200,
565 | failed: response.statusCode !== 200
566 | });
567 | if (results.length === (tests.length * testsLength))
568 | return callback(results);
569 | });
570 | }
571 | });
572 |
573 | };
574 |
575 | Timbit.prototype.paramsAsArray = function() {
576 | var array = [];
577 | for (var key in this.params) {
578 | array.push({key: key, value: this.params[key]});
579 | }
580 | return array;
581 | }
582 |
583 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "timbits",
3 | "description": "Widget framework based on Express",
4 | "version": "0.7.3",
5 | "homepage": "https://github.com/Postmedia/timbits",
6 | "author": "Edward de Groot (http://mred9.com)",
7 | "contributors": [
8 | { "name": "Keith Benedict", "email": "kbenedict@postmedia.com" },
9 | { "name": "Stephen Veerman", "email": "sveerman@postmedia.com" },
10 | { "name": "Donnie Marges", "email": "dmarges@postmedia.com" },
11 | { "name": "Kevin Gamble", "email": "kgamble@postmedia.com" }
12 | ],
13 | "dependencies": {
14 | "winston": "~0.7.1",
15 | "express": "~3.10.5",
16 | "hjs": "~0.0.6",
17 | "hogan.js": "~3.0.1",
18 | "pantry": "~0.7.0",
19 | "request": "~2.34",
20 | "optimist": "~0.3.4",
21 | "run": "~1.2",
22 | "mocha": "~1.20.1",
23 | "should": "~4.0.4",
24 | "which": "~1.0.5",
25 | "should-http": "~0.0.2"
26 | },
27 | "bugs": { "url": "https://github.com/Postmedia/timbits/issues"},
28 | "licenses": [
29 | { "type": "MIT", "url": "https://github.com/Postmedia/timbits/blob/master/LICENSE"}
30 | ],
31 | "keywords": ["framework", "widgets", "express"],
32 | "repository": "git://github.com/postmedia/timbits",
33 | "main": "./lib/timbits",
34 | "bin": {
35 | "timbits": "./bin/timbits"
36 | },
37 | "scripts": {
38 | "test": "mocha"
39 | },
40 | "engines": { "node": ">=0.8.0 <0.11", "npm": "1.x" }
41 | }
42 |
43 |
--------------------------------------------------------------------------------
/resources/images/accept.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Postmedia-Archive/timbits/0213593db6cf94bb35679a4fad01588b19f7fe62/resources/images/accept.png
--------------------------------------------------------------------------------
/resources/images/brick_add.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Postmedia-Archive/timbits/0213593db6cf94bb35679a4fad01588b19f7fe62/resources/images/brick_add.png
--------------------------------------------------------------------------------
/resources/images/cancel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Postmedia-Archive/timbits/0213593db6cf94bb35679a4fad01588b19f7fe62/resources/images/cancel.png
--------------------------------------------------------------------------------
/resources/images/error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Postmedia-Archive/timbits/0213593db6cf94bb35679a4fad01588b19f7fe62/resources/images/error.png
--------------------------------------------------------------------------------
/resources/images/eye.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Postmedia-Archive/timbits/0213593db6cf94bb35679a4fad01588b19f7fe62/resources/images/eye.png
--------------------------------------------------------------------------------
/resources/javascript/timbits.csi.js:
--------------------------------------------------------------------------------
1 | /*
2 | Timbits CSI - v0.6.5
3 | Copyright (c) 2012 Postmedia Network Inc
4 | Written by Edward de Groot (http://mred9.com)
5 | Licensed under the MIT license
6 | https://github.com/Postmedia/timbits
7 | */
8 |
9 | var timbits = timbits || {};
10 |
11 | timbits.renderCSI = function(include, data) {
12 | switch(include.attr('data-mode')) {
13 | case 'append':
14 | include.append(data);
15 | break;
16 | case 'prepend':
17 | include.prepend(data);
18 | break;
19 | default:
20 | include.html(data);
21 | }
22 | include.trigger('load');
23 | }
24 |
25 | timbits.processCSI = function(index, element) {
26 | var include = jQuery(element);
27 | var src = include.attr('data-csi');
28 | var process_this = true;
29 |
30 | // test for optional media query
31 | media = include.attr('data-media');
32 | if (media && window.matchMedia) {
33 | mql = window.matchMedia(media);
34 | process_this = mql.matches;
35 | if (!mql.matches)
36 | mql.addListener(timbits.discoverCSI);
37 | }
38 |
39 | if (process_this) {
40 | // remove data-csi attribute to prevent processing more than once
41 | include.removeAttr('data-csi');
42 |
43 | // dynamic QS insertion
44 | var _re = /{#(\w+)}/;
45 | var match;
46 | while ((match = src.match(_re)) != null) {
47 | src = src.replace(match[0], timbits.csi_qs_params[match[1]] ? timbits.csi_qs_params[match[1]] : '');
48 | }
49 |
50 | console.log('Processing CSI: ' + src);
51 |
52 | if (src.search(/http:|https:/i) === 0)
53 | {
54 | // remote request - add callback parameter
55 | src += (src.indexOf('?') === -1) ? '?' : '&';
56 | src += 'callback=?';
57 |
58 | // fetch the remote resource and render
59 | jQuery.getJSON(src, function(data) {
60 | timbits.renderCSI(include, data);
61 | });
62 | } else {
63 | // fetch the load resource and render
64 | jQuery.get(src, function(data) {
65 | timbits.renderCSI(include, data);
66 | });
67 | }
68 | }
69 | }
70 |
71 | timbits.discoverCSI = function(mql) {
72 | console.log("Discovering CSI: " + (mql ? mql.media : 'Init'));
73 | jQuery('[data-csi]').each(timbits.processCSI);
74 | }
75 |
76 | jQuery( function() {
77 | // parse QS value for optional use within CSI calls
78 | timbits.csi_qs_params = {};
79 |
80 | var csi_qs_regex = /[\?\&](\w+)=([^&]*)/g;
81 | var csi_qs_match;
82 |
83 | while ((csi_qs_match = csi_qs_regex.exec(location.search)) != null) {
84 | timbits.csi_qs_params[csi_qs_match[1]] = csi_qs_match[2];
85 | }
86 | // discover and process CSI calls
87 | timbits.discoverCSI();
88 | });
--------------------------------------------------------------------------------
/resources/javascript/timbits.csi.min.js:
--------------------------------------------------------------------------------
1 | /*
2 | Timbits CSI - v0.6.5
3 | Copyright (c) 2012 Postmedia Network Inc
4 | Written by Edward de Groot (http://mred9.com)
5 | Licensed under the MIT license
6 | https://github.com/Postmedia/timbits
7 | */
8 | var timbits=timbits||{};timbits.renderCSI=function(e,t){switch(e.attr("data-mode")){case"append":e.append(t);break;case"prepend":e.prepend(t);break;default:e.html(t)}e.trigger("load")},timbits.processCSI=function(e,t){var n=jQuery(t),r=n.attr("data-csi"),i=!0;media=n.attr("data-media"),media&&window.matchMedia&&(mql=window.matchMedia(media),i=mql.matches,mql.matches||mql.addListener(timbits.discoverCSI));if(i){n.removeAttr("data-csi");var s=/{#(\w+)}/,o;while((o=r.match(s))!=null)r=r.replace(o[0],timbits.csi_qs_params[o[1]]?timbits.csi_qs_params[o[1]]:"");console.log("Processing CSI: "+r),r.search(/http:|https:/i)===0?(r+=r.indexOf("?")===-1?"?":"&",r+="callback=?",jQuery.getJSON(r,function(e){timbits.renderCSI(n,e)})):jQuery.get(r,function(e){timbits.renderCSI(n,e)})}},timbits.discoverCSI=function(e){console.log("Discovering CSI: "+(e?e.media:"Init")),jQuery("[data-csi]").each(timbits.processCSI)},jQuery(function(){timbits.csi_qs_params={};var e=/[\?\&](\w+)=([^&]*)/g,t;while((t=e.exec(location.search))!=null)timbits.csi_qs_params[t[1]]=t[2];timbits.discoverCSI()});
--------------------------------------------------------------------------------
/resources/stylesheets/timbits.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Postmedia-Archive/timbits/0213593db6cf94bb35679a4fad01588b19f7fe62/resources/stylesheets/timbits.css
--------------------------------------------------------------------------------
/test/timbits-test.js:
--------------------------------------------------------------------------------
1 | // reduce logging levels to provide clean test feedback
2 | process.env.TIMBITS_VERBOSITY = 'critical';
3 | process.env.PANTRY_VERBOSITY = 'critical';
4 |
5 | // load modules
6 | var timbits = require('../lib/timbits')
7 | , should = require('should')
8 | , path = require('path')
9 | , request = require('request')
10 |
11 | // load hhtp extensions for should
12 | require('should-http');
13 |
14 | // set testing environment
15 | var homeFolder = path.join(process.cwd(), "examples")
16 | , port = 8785
17 | , alltests = process.env.TIMBITS_TEST_WHICH === 'all';
18 |
19 | process.env.NODE_ENV = 'test';
20 |
21 | // create test server
22 | var server = timbits.serve({
23 | home: homeFolder,
24 | port: port,
25 | verbosity: 'critical'
26 | });
27 |
28 | // helper function for testing requests
29 | function validateRequest(method, vpath, expect) {
30 | expect = expect || 'html';
31 |
32 | describe(vpath, function() {
33 | var test_msg = "should respond with " + expect + " and status 200";
34 | if (typeof expect !== 'string')
35 | test_msg = "should respond with status " + expect;
36 |
37 | it(test_msg, function(done) {
38 | if (method=="GET") {
39 | request("http://localhost:" + port + vpath, function(err, res) {
40 | should.not.exist(err);
41 | if (typeof expect === 'string') {
42 | res.should.have.status(200);
43 | if (expect === 'json')
44 | res.should.be.json;
45 | else
46 | res.should.be.html;
47 | } else {
48 | res.should.have.status(expect);
49 | }
50 | done();
51 | });
52 | }
53 | else if (method=="POST") {
54 | request.post("http://localhost:" + port + vpath, function(err, res) {
55 | should.not.exist(err);
56 | if (typeof expect === 'string') {
57 | res.should.have.status(200);
58 | if (expect === 'json')
59 | res.should.be.json;
60 | else
61 | res.should.be.html;
62 | } else {
63 | res.should.have.status(expect);
64 | }
65 | done();
66 | });
67 | }
68 | });
69 | });
70 | };
71 |
72 | describe('timbits', function() {
73 |
74 | describe('default resources', function() {
75 | validateRequest('GET', '/timbits/help');
76 | validateRequest('GET', '/timbits/json', 'json');
77 | });
78 |
79 | describe('individual help pages', function() {
80 | for (var name in timbits.box) {
81 | validateRequest('GET', "/" + name + "/help");
82 | }
83 | });
84 |
85 | describe('individual test pages', function() {
86 | for (var name in timbits.box) {
87 | validateRequest('GET', "/" + name + "/test");
88 | }
89 | });
90 |
91 | describe('automated test cases', function() {
92 | for (name in timbits.box) {
93 |
94 | timbit = timbits.box[name];
95 |
96 | // GET requests
97 | if (timbit.examples != null && timbit.methods['GET']) {
98 | describe('specified examples for ' + name, function() {
99 | timbit.examples.forEach(function(example) {
100 | validateRequest('GET', example.href);
101 | });
102 | });
103 | }
104 | // Post Requests
105 | if (timbit.examples != null && timbit.methods['POST']) {
106 | describe('specified examples for ' + name, function() {
107 | timbit.examples.forEach(function(example) {
108 | validateRequest('POST', example.href);
109 | });
110 | });
111 | }
112 | var dynatests = timbit.generateTests(alltests);
113 | if (dynatests.length) {
114 | describe("dynamic tests for " + name, function() {
115 | dynatests.forEach(function(href) {
116 | if (timbit.methods['GET'])
117 | validateRequest('GET', href);
118 | if (timbit.methods['POST'])
119 | validateRequest('POST', href);
120 | });
121 | });
122 | }
123 | }
124 | });
125 |
126 | });
--------------------------------------------------------------------------------