├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── error.html ├── helpers.js ├── index.js ├── package.json ├── pagelet.fragment └── test ├── common.js ├── fixtures ├── error.html ├── style.css ├── view.html └── view.jsx ├── helpers.test.js ├── mocha.opts └── pagelet.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .coveralls.yml 4 | coverage -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "0.11" 5 | - "0.10" 6 | - "0.9" 7 | - "iojs-v1.1" 8 | - "iojs-v1.0" 9 | before_install: 10 | - "npm install -g npm@2.1.18" 11 | script: 12 | - "npm run test-travis" 13 | after_script: 14 | - "npm install coveralls@2.11.x && cat coverage/lcov.info | coveralls" 15 | matrix: 16 | fast_finish: true 17 | allow_failures: 18 | - node_js: "0.11" 19 | - node_js: "0.9" 20 | - node_js: "iojs-v1.1" 21 | - node_js: "iojs-v1.0" 22 | notifications: 23 | irc: 24 | channels: 25 | - "irc.freenode.org#bigpipe" 26 | on_success: change 27 | on_failure: change 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Arnout Kazemier, Martijn Swaagman, the Contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pagelet 2 | 3 | [![Version npm][version]](http://browsenpm.org/package/pagelet)[![Build Status][build]](https://travis-ci.org/bigpipe/pagelet)[![Dependencies][david]](https://david-dm.org/bigpipe/pagelet)[![Coverage Status][cover]](https://coveralls.io/r/bigpipe/pagelet?branch=master) 4 | 5 | [version]: http://img.shields.io/npm/v/pagelet.svg?style=flat-square 6 | [build]: http://img.shields.io/travis/bigpipe/pagelet/master.svg?style=flat-square 7 | [david]: https://img.shields.io/david/bigpipe/pagelet.svg?style=flat-square 8 | [cover]: http://img.shields.io/coveralls/bigpipe/pagelet/master.svg?style=flat-square 9 | 10 | ## Installation 11 | 12 | There are two different ways of using Pagelet in your project. If you're already 13 | using the [BigPipe] framework you don't need to install anything as this module 14 | is exposed using: 15 | 16 | ```js 17 | var Pagelet = require('bigpipe').Pagelet; 18 | ``` 19 | 20 | If you want to build stand-alone pagelets to be used in BigPipe or just want to 21 | use the Pagelet pattern in your application you need to install the module your 22 | self using: 23 | 24 | ``` 25 | npm install --save pagelet 26 | ``` 27 | 28 | And require it in your application as: 29 | 30 | ```js 31 | var Pagelet = require('pagelet'); 32 | ``` 33 | 34 | Which is also the code as we assume in all the examples in our documentation. 35 | 36 | ## Table of Contents 37 | 38 | **Pagelet function** 39 | - [Pagelet.extend](#pageletextend) 40 | - [Pagelet.on](#pageleton) 41 | - [Pagelet.traverse](#pagelettraverse) 42 | 43 | **Pagelet instance** 44 | - [Pagelet.name](#pageletname) 45 | - [Pagelet.streaming](#pageletstreaming) 46 | - [Pagelet.RPC](#pageletrpc) 47 | - [Pagelet.mode](#pageletmode) 48 | - [Pagelet.fragment](#pageletfragment) 49 | - [Pagelet.remove](#pageletremove) 50 | - [Pagelet.view](#pageletview) 51 | - [Pagelet.error](#pageleterror) 52 | - [Pagelet.engine](#pageletengine) 53 | - [Pagelet.query](#pageletquery) 54 | - [Pagelet.css](#pageletcss) 55 | - [Pagelet.js](#pageletjs) 56 | - [Pagelet.dependencies](#pageletdependencies) 57 | - [Pagelet.get()](#pageletget) 58 | - [Pagelet.authorize()](#pageletauthorize) 59 | - [Pagelet.initialize()](#pageletinitialize) 60 | - [Pagelet.pagelets](#pageletpagelets) 61 | - [Pagelet.id](#pageletid) 62 | - [Pagelet.substream](#pageletsubstream) 63 | - [Pagelet._parent](#pageletparent) 64 | 65 | ### Pagelet.extend 66 | 67 | The `.extend` method is used for creating a new Pagelet constructor. It 68 | subclasses the `Pagelet` constructor just like you're used to when using 69 | [Backbone]. It accepts an object which will be automatically applied as part of 70 | the prototype: 71 | 72 | ```js 73 | Pagelet.extend({ 74 | js: 'client.js', 75 | css: 'sidebar.styl', 76 | view: 'templ.jade', 77 | 78 | get: function get() { 79 | // do stuff when GET is called via render 80 | } 81 | }); 82 | ``` 83 | 84 | ### Pagelet.on 85 | 86 | In [BigPipe] we need to know where the Pagelet is required from so we figure out 87 | how to correctly resolve the relative paths of the `css`, `js` and `view` 88 | properties. 89 | 90 | So a full constructed Pagelet instance looks like: 91 | 92 | ```js 93 | Pagelet.extend({ 94 | my: 'prop', 95 | and: function () {} 96 | }).on(module); 97 | ``` 98 | 99 | This has the added benefit of no longer needing to do `module.exports = ..` in 100 | your code as the `Pagelet.on` method automatically does this for you. 101 | 102 | ### Pagelet.traverse 103 | 104 | Recursively find and construct all pagelets. Uses the 105 | [pagelets property](#pageletpagelets) to find additional child pagelets. Usually 106 | there is no need to call this manually. [BigPipe] will make sure all pagelets 107 | are recursively discovered. Traverse should be called with the name of the 108 | parent pagelet, so each child has a proper reference. 109 | 110 | ``` 111 | Pagelet.extend({ 112 | name: 'parent name', 113 | pagelets: { 114 | one: require('pagelet'), 115 | two: require('pagelet') 116 | } 117 | }).traverse('parent name'); 118 | ``` 119 | 120 | ### Pagelet.name 121 | 122 | _required:_ **writable, string** 123 | 124 | Every pagelet should have a name, it's one of the ways that [BigPipe] uses to 125 | identify which pagelet and where it should be loaded on the page. The name 126 | should be an unique but human readable string as this will be used as value for 127 | the `data-pagelet=""` attributes on your [Page], but this name is also when you 128 | want to check if a `Pagelet` is available. 129 | 130 | ```js 131 | Pagelet.extend({ 132 | name: 'sidebar' 133 | }).on(module); 134 | ``` 135 | 136 | If no `name` property has been set on the Pagelet it will take the `key` that 137 | was used when you specified the pagelets for the [Page]: 138 | 139 | ```js 140 | var Page = require('bigpipe').Page; 141 | 142 | Page.extend({ 143 | pagelets: { 144 | sidebar: '../yourpagelet.js', 145 | another: require('../yourpagelet.js') 146 | } 147 | }).on(module); 148 | ``` 149 | 150 | If you supplied the [Page] instance if a path to a folder of pagelet folders it 151 | will use the name of the folders: 152 | 153 | ```js 154 | var Page = require('bigpipe').Page; 155 | 156 | Page.extend({ 157 | pagelets: './pagelets-folder' 158 | }).on(module); 159 | ``` 160 | ``` 161 | |- page.js 162 | |- pagelets-folder/ 163 | | 164 | |- foo/ 165 | |- bar/ 166 | |- baz/ 167 | ``` 168 | 169 | So in the example above you would have 3 pagelets with the names `foo`, `bar` and 170 | `baz`. 171 | 172 | ### Pagelet.streaming 173 | 174 | _optional:_ **writable, boolean** 175 | 176 | When enabled we will stream the submit of each form that is within a Pagelet to 177 | the server instead of using the default full page refreshes. After sending the 178 | data the resulting HTML will be used to only update the contents of the pagelet. 179 | 180 | If you want to opt-out of this with one form you can add a 181 | `data-pagelet-async="false"` attribute to the form element. 182 | 183 | **Default value**: `false` 184 | 185 | ```js 186 | Pagelet.extend({ 187 | streaming: true 188 | }); 189 | ``` 190 | 191 | ### Pagelet.RPC 192 | 193 | _optional:_ **writable, array** 194 | 195 | The `RPC` array specifies the methods that can be remotely called from the 196 | client/browser. Please note that they are not actually send to the client as 197 | these functions will execute on the server and transfer the result back to the 198 | client. 199 | 200 | The first argument that these functions receive is an error first style callback 201 | which is used to transfer the response back to the client. All other arguments 202 | will be the arguments that were used to call the method on the client. 203 | 204 | **Default value**: `[]` 205 | 206 | ```js 207 | Pagelet.extend({ 208 | RPC: [ 'methodname' ], 209 | 210 | methodname: function methodname(reply, arg1, arg2) { 211 | 212 | } 213 | }).on(module); 214 | ``` 215 | 216 | ### Pagelet.mode 217 | 218 | _optional:_ **writable, string** 219 | 220 | Set the render mode the pagelet fragment. This will determine which client side 221 | method will be called to create elements. For instance, this mode can be changed 222 | to `svg` to generate SVG elements with the SVG namespaceURI. 223 | 224 | **Default value**: `html` 225 | 226 | ```js 227 | Pagelet.extend({ 228 | mode: 'svg', 229 | }).on(module); 230 | ``` 231 | 232 | We currently support two different render modes: 233 | 234 | - **html**: Render HTML elements. 235 | - **svg**: Render SVG elements. 236 | 237 | ### Pagelet.fragment 238 | 239 | _optional:_ **writable, string** 240 | 241 | A default fragment is provided via `Pagelet.fragment`, however it is 242 | possible to overwrite this default fragment with a custom fragment. This fragment 243 | is used by render to generate content with appropriate data to work with [BigPipe]. 244 | Change `Pagelet.fragment` if you'd like to invoke render and generate custom output. 245 | 246 | **Default value**: see [pagelet.fragment][frag] 247 | 248 | ```js 249 | Pagelet.extend({ 250 | fragment: '
{pagelet:template}
', 251 | }).on(module); 252 | ``` 253 | 254 | The received fragment can contain various of placeholders which will be replaced 255 | before the template is flushed to the browser. The following placeholders are 256 | supported: 257 | 258 | - `{pagelet:template}` This contains the rendered output of your specified view. 259 | - `{pagelet:name}` The name of pagelet we're currently rendering. 260 | - `{pagelet:data}` A JSON string blob of meta data about the pagelet which contains: 261 | - `id`: String, A unique id of the pagelet that was rendered. 262 | - `mode`: String, the render mode that you've configured. 263 | - `rpc`: Array, names of the RPC methods. 264 | - `remove`: Boolean, should this be removed from the DOM. 265 | - `streaming`: Boolean, should we stream form submits 266 | - `parent`: String, name of the parent pagelet. 267 | - `hash`: Object, containing the MD5 hashes of the client view. 268 | 269 | ### Pagelet.remove 270 | 271 | _optional:_ **writable, boolean** 272 | 273 | This instructs our render engine to remove the pagelet placeholders from the DOM 274 | structure if we've got no pagelets available for it. This makes it easier to 275 | create conditional layouts without having to worry about DOM elements that are 276 | left behind. 277 | 278 | **Default value**: `true` 279 | 280 | ```js 281 | Pagelet.extend({ 282 | if: function conditional(req, next) { 283 | next(false); 284 | }, 285 | remove: false 286 | }).on(module); 287 | ``` 288 | 289 | ### Pagelet.view 290 | 291 | _required:_ **writable, string** 292 | 293 | The view is a reference to the template that we render inside the 294 | `data-pagelet=""` placeholders. Please make sure that your template can be 295 | rendered on both the client and server side. Take a look at our [temper] project 296 | for template engines that we support. 297 | 298 | ### Pagelet.error 299 | 300 | _optional:_ **writable, string** 301 | 302 | Just like the `Pagelet.view` this is a reference to a template that we will 303 | render in your `data-pagelet=""` placeholders but this template is only 304 | rendered when: 305 | 306 | 1. We receive an `Error` argument in our callback that we supply to the 307 | `Pagelet#get` method. 308 | 2. Your `Pagelet.view` throws an error when we're rendering the template. 309 | 310 | If this property is not set we will default to a template that ships with this 311 | Pagelet by default. This template includes a small HTML fragment that states the 312 | error. 313 | 314 | ### Pagelet.engine 315 | 316 | _optional:_ **writable, string** 317 | 318 | We attempt to detect the correct template engine based on filename as well as 319 | the template engine's that we can require. It is possible that we make the wrong 320 | assumption and you wanted to use `handlebars` for your `.mustache` based 321 | templates but it choose to use `hogan.js` instead. 322 | 323 | ```js 324 | Pagelet.extend({ 325 | view: 'sidebar.mustache', 326 | engine: 'handlebars' 327 | }).on(module); 328 | ``` 329 | 330 | **Please note that the engine needs to be compatible with the [temper] module 331 | that we use to compile the templates** 332 | 333 | ### Pagelet.query 334 | 335 | _optional:_ **writable, array** 336 | 337 | For optimal performance the data that is send to the client will be minimal 338 | and dependant on they query that is provided. Data can be supplied to the client 339 | by listing the keys (nested paths in dot notation) of which the data should be 340 | send to the client. In the example only the content of `mydata` and `nested.is` 341 | will be send. 342 | 343 | ```js 344 | Pagelet.extend({ 345 | query: [ 'mydata', 'nested.is' ], 346 | get: function get(done) { 347 | done(null, { 348 | mydata: 'test', 349 | nested: { is: 'allowed', left: 'alone' }, 350 | more: 'data' 351 | }); 352 | } 353 | }).on(module); 354 | ``` 355 | 356 | ### Pagelet.css 357 | 358 | _optional:_ **writable, string** 359 | 360 | The location of the styling for **only this** pagelet. You should assume that 361 | you bundle all the CSS that is required to fully render this pagelet. By 362 | eliminating inherited CSS it will be easier for you to re-use this pagelet on 363 | other pages as well as in other projects. 364 | 365 | ```js 366 | Pagelet.extend({ 367 | css: './my-little-pony.styl' 368 | }).on(module); 369 | ``` 370 | 371 | **Please note that this doesn't have to be a `.css` file as we will 372 | transparently pre-process these files for you. See the [smithy] project for the 373 | compatible pre-processors.** 374 | 375 | ### Pagelet.js 376 | 377 | _optional:_ **writable, string** 378 | 379 | As you might have guessed, this is the location of the JavaScript that you want 380 | to have loaded for your pagelet. We use [fortress] to sandbox this JavaScript in 381 | a dedicated `iframe` so the code you write is not affected and will not affect 382 | other pagelets on the same page. This also makes it relatively save to extend 383 | the build-in primitives of JavaScript (adding new properties to Array etc). 384 | 385 | Unlike the `view` and `css` we do not pre-process the JavaScript. But this does 386 | not mean you cannot use CoffeeScript or other pre-processed languages inside a 387 | Pagelet. It just means that you have to compile your files to a proper 388 | JavaScript file and point to that location instead. 389 | 390 | ```js 391 | Pagelet.extend({ 392 | js: './library.js' 393 | }).on(module); 394 | ``` 395 | 396 | **Please note that the sandboxing is not there as a security feature, it was 397 | only designed to prevent code from different pagelets clashing with each other** 398 | 399 | ### Pagelet.dependencies 400 | 401 | _optional:_ **writable, array** 402 | 403 | An array of dependencies that your pagelet depends on which should be loaded in 404 | advance and available on the page before any CSS or JavaScript is executed. The 405 | files listed in this array can either a be CSS or JavaScript resource. 406 | 407 | ```js 408 | pagelet.extend({ 409 | dependencies: [ 410 | 'https://google.com/ga.js' 411 | ] 412 | }).on(module); 413 | ``` 414 | 415 | ### Pagelet.get() 416 | 417 | _required:_ **writable, function** 418 | 419 | Get provides the data that is used for rendering the output of the Pagelet. 420 | 421 | The `get` method receives one argument: 422 | 423 | - done: A completion callback which accepts two arguments. This callback should be 424 | called when your custom implementation has finished gathering data from all sources. 425 | Calling `done(error, data)` will allow the `render` method to complete its work. 426 | The data provided to the callback will be used to render the actual Pagelet. 427 | 428 | ```js 429 | Pagelet.extend({ 430 | get: function get(done) { 431 | var data = { provide: 'data-async' }; 432 | done(error, data); 433 | }, 434 | }).on(module); 435 | ``` 436 | 437 | ### Pagelet.if() 438 | 439 | _optional:_ **writable, function** 440 | 441 | The `if` function allows you to build conditional pagelets. These pagelets will 442 | only be rendered if the supplied callback receives `true`. This can be used to 443 | build private pagelets like administrator pagelets that require special 444 | permissions in order to be shown seen. 445 | 446 | When used in [BigPipe] we take this concept even further as it's possible to set 447 | an array of pagelets that could be used in the placeholder. You could use to 448 | show login and logout buttons, sign up or getting starting pagelets or even 449 | start doing A/B testing with multiple pagelets! The possibilities are endless 450 | here. 451 | 452 | The supplied function receives 2 or 3 arguments: 453 | - req: The incoming HTTP requirement. 454 | - left: An array of pagelets that will tried if this pagelet callback resolves 455 | to false. This is an optional argument, if you do no specify it your last 456 | argument will be the completion callback that is listed below. 457 | - done: A completion callback which only accepts one argument, a boolean. If 458 | this boolean has been set to `true` the pagelet is authorized on the page and 459 | will be rendered as expected. When the argument evaluates as `false` (so also 460 | null, undefined, 0 etc) we assume that it's disallowed and should not be 461 | rendered. 462 | 463 | ```js 464 | Pagelet.extend({ 465 | if: function conditional(req, done) { 466 | done(true); // True indicates that the request is authorized for access. 467 | } 468 | }).on(module); 469 | ``` 470 | 471 | Or with 3 arguments: 472 | 473 | ```js 474 | Pagelet.extend({ 475 | if: function abtest(req, left, done) { 476 | if (!left.length) return done(true); 477 | done(Math.random() < 0.5); 478 | } 479 | }).on(module); 480 | ``` 481 | 482 | ### Pagelet.initialize() 483 | 484 | _optional:_ **writable, function** 485 | 486 | The pagelet has been initialised. If you have an authorization function this 487 | function will only be called **after** a successful authorization. If no 488 | authorization hook is provided it should be called instantly. 489 | 490 | ```js 491 | Pagelet.extend({ 492 | initialize: function () { 493 | this.once('event', function () { 494 | doStuff(); 495 | }); 496 | } 497 | }); 498 | ``` 499 | 500 | ### Pagelet.pagelets 501 | 502 | _optional:_ **writable, string|array|object** 503 | 504 | Each pagelet can contain `n` child pagelets. Similar to using pagelets through 505 | [BigPipe], the pagelets property can be a string (filepath to file or directory), 506 | array or object containing multiple pagelets. All subsequent child pagelets will 507 | be converged on one stack to allow full parallel initialization. The client will 508 | handle deferred rendering of child pagelets, also see [_parent](#pageletparent). 509 | 510 | ``` 511 | Pagelet.extend({ 512 | pagelets: { 513 | one: require('pagelet'), 514 | two: require('pagelet') 515 | } 516 | }); 517 | ``` 518 | 519 | ### Pagelet.id 520 | 521 | **read only** 522 | 523 | The unique id of a given pagelet instance. Please note that this is not a 524 | persistent id and will differ between every single initialised instance. 525 | 526 | ### Pagelet.substream 527 | 528 | **read only** 529 | 530 | The pagelet can also be initialised through [Primus] so it can be used for 531 | real-time communication (and make things like [RPC](#pagelet-rpc) work). The 532 | communication is done over a [substream] which allows Primus multiplex the 533 | connection between various of endpoints. 534 | 535 | ### Pagelet._parent 536 | 537 | **read only** 538 | 539 | If the current pagelet is intialized from another pagelet, it will have a `_parent` 540 | reference. The pagelets' parent name will be stored so that client-side 541 | initialization is deferred till the parent is rendered. 542 | 543 | ## License 544 | 545 | MIT 546 | 547 | [Backbone]: http://backbonejs.com 548 | [BigPipe]: http://bigpipe.io 549 | [Page]: http://bigpipe.io#page 550 | [temper]: http://github.com/bigpipe/temper 551 | [smithy]: http://github.com/observing/smithy 552 | [fortress]: http://github.com/bigpipe/fortress 553 | [frag]: https://github.com/bigpipe/pagelet/blob/master/pagelet.fragment 554 | [Primus]: https://github.com/primus/primus 555 | [substream]: https://github.com/primus/substream 556 | -------------------------------------------------------------------------------- /error.html: -------------------------------------------------------------------------------- 1 |

Error

2 | 3 |

{reason}, {message}

4 | -------------------------------------------------------------------------------- /helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | 5 | /** 6 | * Helper function to resolve assets on the pagelet. 7 | * 8 | * @param {Function} constructor The Pagelet constructor 9 | * @param {String|Array} keys Name(s) of the property, e.g. [css, js]. 10 | * @param {String} dir Optional absolute directory to resolve from. 11 | * @returns {Pagelet} 12 | * @api private 13 | */ 14 | exports.resolve = function resolve(constructor, keys, dir) { 15 | var prototype = constructor.prototype; 16 | 17 | keys = Array.isArray(keys) ? keys : [keys]; 18 | keys.forEach(function each(key) { 19 | if (!prototype[key]) return; 20 | 21 | var stack = Array.isArray(prototype[key]) 22 | ? prototype[key] 23 | : [prototype[key]]; 24 | 25 | prototype[key] = stack.filter(Boolean).map(function map(file) { 26 | if (/^(http:|https:)?\/\//.test(file)) return file; 27 | return path.resolve(dir || prototype.directory, file); 28 | }); 29 | }); 30 | 31 | return constructor; 32 | }; 33 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Formidable = require('formidable').IncomingForm 4 | , fabricate = require('fabricator') 5 | , helpers = require('./helpers') 6 | , debug = require('diagnostics') 7 | , dot = require('dot-component') 8 | , destroy = require('demolish') 9 | , Route = require('routable') 10 | , fuse = require('fusing') 11 | , async = require('async') 12 | , path = require('path') 13 | , url = require('url'); 14 | 15 | // 16 | // Cache long prototype lookups to increase speed + write shorter code. 17 | // 18 | var slice = Array.prototype.slice; 19 | 20 | // 21 | // Methods that needs data buffering. 22 | // 23 | var operations = 'POST, PUT, DELETE, PATCH'.toLowerCase().split(', '); 24 | 25 | /** 26 | * Simple helper function to generate some what unique id's for given 27 | * constructed pagelet. 28 | * 29 | * @returns {String} 30 | * @api private 31 | */ 32 | function generator(n) { 33 | if (!n) return Date.now().toString(36).toUpperCase(); 34 | return Math.random().toString(36).substring(2, 10).toUpperCase(); 35 | } 36 | 37 | /** 38 | * A pagelet is the representation of an item, section, column or widget. 39 | * It's basically a small sandboxed application within your application. 40 | * 41 | * @constructor 42 | * @param {Object} options Optional configuration. 43 | * @api public 44 | */ 45 | function Pagelet(options) { 46 | if (!this) return new Pagelet(options); 47 | 48 | this.fuse(); 49 | options = options || {}; 50 | 51 | // 52 | // Use the temper instance on Pipe if available. 53 | // 54 | if (options.bigpipe && options.bigpipe._temper) { 55 | options.temper = options.bigpipe._temper; 56 | } 57 | 58 | this.writable('_enabled', []); // Contains all enabled pagelets. 59 | this.writable('_disabled', []); // Contains all disable pagelets. 60 | this.writable('_active', null); // Are we active. 61 | this.writable('_req', options.req); // Incoming HTTP request. 62 | this.writable('_res', options.res); // Incoming HTTP response. 63 | this.writable('_params', options.params); // Params extracted from the route. 64 | this.writable('_temper', options.temper); // Attach the Temper instance. 65 | this.writable('_bigpipe', options.bigpipe); // Actual pipe instance. 66 | this.writable('_bootstrap', options.bootstrap); // Reference to bootstrap Pagelet. 67 | this.writable('_append', options.append || false); // Append content client-side. 68 | 69 | this.writable('debug', debug('pagelet:'+ this.name)); // Namespaced debug method 70 | 71 | // 72 | // Allow overriding the reference to parent pagelet. 73 | // A reference to the parent is normally set on the 74 | // constructor prototype by optimize. 75 | // 76 | if (options.parent) this.writable('_parent', options.parent); 77 | } 78 | 79 | fuse(Pagelet, require('eventemitter3')); 80 | 81 | /** 82 | * Unique id, useful for internal querying. 83 | * 84 | * @type {String} 85 | * @public 86 | */ 87 | Pagelet.writable('id', null); 88 | 89 | /** 90 | * The name of this pagelet so it can checked to see if's enabled. In addition 91 | * to that, it can be injected in to placeholders using this name. 92 | * 93 | * @type {String} 94 | * @public 95 | */ 96 | Pagelet.writable('name', ''); 97 | 98 | /** 99 | * The HTTP pathname that we should be matching against. 100 | * 101 | * @type {String|RegExp} 102 | * @public 103 | */ 104 | Pagelet.writable('path', null); 105 | 106 | /** 107 | * Which HTTP methods should this pagelet accept. It can be a comma 108 | * separated string or an array. 109 | * 110 | * @type {String|Array} 111 | * @public 112 | */ 113 | Pagelet.writable('method', 'GET'); 114 | 115 | /** 116 | * The default status code that we should send back to the user. 117 | * 118 | * @type {Number} 119 | * @public 120 | */ 121 | Pagelet.writable('statusCode', 200); 122 | 123 | /** 124 | * The pagelets that need to be loaded as children of this pagelet. 125 | * 126 | * @type {Object} 127 | * @public 128 | */ 129 | Pagelet.writable('pagelets', {}); 130 | 131 | /** 132 | * With what kind of generation mode do we need to output the generated 133 | * pagelets. We're supporting 3 different modes: 134 | * 135 | * - sync: Fully render without any fancy flushing of pagelets. 136 | * - async: Render all pagelets async and flush them as fast as possible. 137 | * - pipeline: Same as async but in the specified order. 138 | * 139 | * @type {String} 140 | * @public 141 | */ 142 | Pagelet.writable('mode', 'async'); 143 | 144 | /** 145 | * Save the location where we got our resources from, this will help us with 146 | * fetching assets from the correct location. 147 | * 148 | * @type {String} 149 | * @public 150 | */ 151 | Pagelet.writable('directory', ''); 152 | 153 | /** 154 | * The environment that we're running this pagelet in. If this is set to 155 | * `development` It would be verbose. 156 | * 157 | * @type {String} 158 | * @public 159 | */ 160 | Pagelet.writable('env', (process.env.NODE_ENV || 'development').toLowerCase()); 161 | 162 | /** 163 | * Conditionally load this pagelet. It can also be used as authorization handler. 164 | * If the incoming request is not authorized you can prevent this pagelet from 165 | * showing. The assigned function receives 3 arguments. 166 | * 167 | * - req, the http request that initialized the pagelet 168 | * - list, array of pagelets that will be tried 169 | * - done, a callback function that needs to be called with only a boolean. 170 | * 171 | * ```js 172 | * Pagelet.extend({ 173 | * if: function conditional(req, list, done) { 174 | * done(true); // True indicates that the request is authorized for access. 175 | * } 176 | * }); 177 | * ``` 178 | * 179 | */ 180 | Pagelet.writable('if', null); 181 | 182 | /** 183 | * A pagelet has been initialized. 184 | * 185 | * @type {Function} 186 | * @public 187 | */ 188 | Pagelet.writable('initialize', null); 189 | 190 | /** 191 | * Remove the DOM element if we are not enabled. This will make it easier to 192 | * create conditional layouts without having to manage the pointless DOM 193 | * elements. 194 | * 195 | * @type {Boolean} 196 | * @public 197 | */ 198 | Pagelet.writable('remove', true); 199 | 200 | /** 201 | * List of keys in the data that will be supplied to the client-side script. 202 | * Paths to nested keys can be supplied via dot notation. 203 | * 204 | * @type {Array} 205 | * @public 206 | */ 207 | Pagelet.writable('query', []); 208 | 209 | /** 210 | * The location of your view template. But just because you've got a view 211 | * template it doesn't mean we will render it. It depends on how the pagelet is 212 | * called. If it's called from the client side we will only forward the data to 213 | * server. 214 | * 215 | * As a user you need to make sure that your template runs on the client as well 216 | * as on the server side. 217 | * 218 | * @type {String} 219 | * @public 220 | */ 221 | Pagelet.writable('view', null); 222 | 223 | /** 224 | * The location of your error template. This template will be rendered when: 225 | * 226 | * 1. We receive an `error` argument from your `get` method. 227 | * 2. Your view throws an error when rendering the template. 228 | * 229 | * If no view has been set it will default to the Pagelet's default error 230 | * template which outputs a small HTML fragment that states the error. 231 | * 232 | * @type {String} 233 | * @public 234 | */ 235 | Pagelet.writable('error', path.join(__dirname, 'error.html')); 236 | 237 | /** 238 | * Optional template engine preference. Useful when we detect the wrong template 239 | * engine based on the view's file name. If no engine is provide we will attempt 240 | * to figure out the correct template engine based on the file extension of the 241 | * provided template path. 242 | * 243 | * @type {String} 244 | * @public 245 | */ 246 | Pagelet.writable('engine', ''); 247 | 248 | /** 249 | * The Style Sheet for this pagelet. The location can be a string or multiple paths 250 | * in an array. It should contain all the CSS that's needed to render this pagelet. 251 | * It doesn't have to be a `CSS` extension as these files are passed through 252 | * `smithy` for automatic pre-processing. 253 | * 254 | * @type {String|Array} 255 | * @public 256 | */ 257 | Pagelet.writable('css', ''); 258 | 259 | /** 260 | * The JavaScript files needed for this pagelet. The location can be a string or 261 | * multiple paths in an array. This file needs to be included in order for 262 | * this pagelet to function. 263 | * 264 | * @type {String|Array} 265 | * @public 266 | */ 267 | Pagelet.writable('js', ''); 268 | 269 | /** 270 | * An array with dependencies that your pagelet depends on. This can be CSS or 271 | * JavaScript files/frameworks whatever. It should be an array of strings 272 | * which represent the location of these files. 273 | * 274 | * @type {Array} 275 | * @public 276 | */ 277 | Pagelet.writable('dependencies', []); 278 | 279 | /** 280 | * Save the location where we got our resources from, this will help us with 281 | * fetching assets from the correct location. This property is automatically set 282 | * when the you do: 283 | * 284 | * ```js 285 | * Pagelet.extend({}).on(module); 286 | * ``` 287 | * 288 | * If you do not use this pattern make sure you set an absolute path the 289 | * directory that the pagelet and all it's resources are in. 290 | * 291 | * @type {String} 292 | * @public 293 | */ 294 | Pagelet.writable('directory', ''); 295 | 296 | /** 297 | * Reference to parent Pagelet name. 298 | * 299 | * @type {Object} 300 | * @private 301 | */ 302 | Pagelet.writable('_parent', null); 303 | 304 | /** 305 | * Set of optimized children Pagelet. 306 | * 307 | * @type {Object} 308 | * @private 309 | */ 310 | Pagelet.writable('_children', {}); 311 | 312 | /** 313 | * Cataloged dependencies by extension. 314 | * 315 | * @type {Object} 316 | * @private 317 | */ 318 | Pagelet.writable('_dependencies', {}); 319 | 320 | /** 321 | * Default character set, UTF-8. 322 | * 323 | * @type {String} 324 | * @private 325 | */ 326 | Pagelet.writable('_charset', 'UTF-8'); 327 | 328 | /** 329 | * Default content type of the Pagelet. 330 | * 331 | * @type {String} 332 | * @private 333 | */ 334 | Pagelet.writable('_contentType', 'text/html'); 335 | 336 | /** 337 | * Default asynchronous get function. Override to provide specific data to the 338 | * render function. 339 | * 340 | * @param {Function} done Completion callback when we've received data to render. 341 | * @api public 342 | */ 343 | Pagelet.writable('get', function get(done) { 344 | (global.setImmediate || global.setTimeout)(done); 345 | }); 346 | 347 | /** 348 | * Get parameters that were extracted from the route. 349 | * 350 | * @type {Object} 351 | * @public 352 | */ 353 | Pagelet.readable('params', { 354 | enumerable: false, 355 | get: function params() { 356 | return this._params || this.bootstrap._params || Object.create(null); 357 | } 358 | }, true); 359 | 360 | /** 361 | * Report the length of the queue (e.g. amount of children). The length 362 | * is increased with one as the reporting pagelet is part of the queue. 363 | * 364 | * @return {Number} Length of queue. 365 | * @api private 366 | */ 367 | Pagelet.get('length', function length() { 368 | return this._children.length; 369 | }); 370 | 371 | /** 372 | * Get and initialize a given child Pagelet. 373 | * 374 | * @param {String} name Name of the child pagelet. 375 | * @returns {Array} The pagelet instances. 376 | * @api public 377 | */ 378 | Pagelet.readable('child', function child(name) { 379 | if (Array.isArray(name)) name = name[0]; 380 | return (this.has(name) || this.has(name, true) || []).slice(0); 381 | }); 382 | 383 | /** 384 | * Helper to invoke a specific route with an optionally provided method. 385 | * Useful for serving a pagelet after handling POST requests for example. 386 | * 387 | * @param {String} route Registered path. 388 | * @param {String} method Optional HTTP verb. 389 | * @returns {Pagelet} fluent interface. 390 | */ 391 | Pagelet.readable('serve', function serve(route, method) { 392 | var req = this._req 393 | , res = this._res; 394 | 395 | req.method = (method || 'get').toUpperCase(); 396 | req.uri = url.parse(route); 397 | 398 | this._bigpipe.router(req, res); 399 | return this; 400 | }); 401 | 402 | /** 403 | * Helper to check if the pagelet has a child pagelet by name, must use 404 | * prototype.name since pagelets are not always constructed yet. 405 | * 406 | * @param {String} name Name of the pagelet. 407 | * @param {String} enabled Make sure that we use the enabled array. 408 | * @returns {Array} The constructors of matching Pagelets. 409 | * @api public 410 | */ 411 | Pagelet.readable('has', function has(name, enabled) { 412 | if (!name) return []; 413 | 414 | if (enabled) return this._enabled.filter(function filter(pagelet) { 415 | return pagelet.name === name; 416 | }); 417 | 418 | var pagelets = this._children 419 | , i = pagelets.length 420 | , pagelet; 421 | 422 | while (i--) { 423 | pagelet = pagelets[i][0]; 424 | 425 | if ( 426 | pagelet.prototype && pagelet.prototype.name === name 427 | || pagelets.name === name 428 | ) return pagelets[i]; 429 | } 430 | 431 | return []; 432 | }); 433 | 434 | /** 435 | * Render execution flow. 436 | * 437 | * @api private 438 | */ 439 | Pagelet.readable('init', function init() { 440 | var method = this._req.method.toLowerCase() 441 | , pagelet = this; 442 | 443 | // 444 | // Only start reading the incoming POST request when we accept the incoming 445 | // method for read operations. Render in a regular mode if we do not accept 446 | // these requests. 447 | // 448 | if (~operations.indexOf(method)) { 449 | var pagelets = this.child(this._req.query._pagelet) 450 | , reader = this.read(pagelet); 451 | 452 | this.debug('Processing %s request', method); 453 | 454 | async.whilst(function work() { 455 | return !!pagelets.length; 456 | }, function process(next) { 457 | var Child = pagelets.shift() 458 | , child; 459 | 460 | if (!(method in Pagelet.prototype)) return next(); 461 | 462 | child = new Child({ bigpipe: pagelet._bigpipe }); 463 | child.conditional(pagelet._req, pagelets, function allowed(accepted) { 464 | if (!accepted) { 465 | if (child.destroy) child.destroy(); 466 | return next(); 467 | } 468 | 469 | reader.before(child[method], child); 470 | }); 471 | }, function nothing() { 472 | if (method in pagelet) { 473 | reader.before(pagelet[method], pagelet); 474 | } else { 475 | pagelet._bigpipe[pagelet.mode](pagelet); 476 | } 477 | }); 478 | } else { 479 | this._bigpipe[this.mode](this); 480 | } 481 | }); 482 | 483 | /** 484 | * Start buffering and reading the incoming request. 485 | * 486 | * @returns {Form} 487 | * @api private 488 | */ 489 | Pagelet.readable('read', function read() { 490 | var form = new Formidable 491 | , pagelet = this 492 | , fields = {} 493 | , files = {} 494 | , context 495 | , before; 496 | 497 | form.on('progress', function progress(received, expected) { 498 | // 499 | // @TODO if we're not sure yet if we should handle this form, we should only 500 | // buffer it to a predefined amount of bytes. Once that limit is reached we 501 | // need to `form.pause()` so the client stops uploading data. Once we're 502 | // given the heads up, we can safely resume the form and it's uploading. 503 | // 504 | }).on('field', function field(key, value) { 505 | fields[key] = value; 506 | }).on('file', function file(key, value) { 507 | files[key] = value; 508 | }).on('error', function error(err) { 509 | pagelet.capture(err, true); 510 | fields = files = {}; 511 | }).on('end', function end() { 512 | form.removeAllListeners(); 513 | 514 | if (before) { 515 | before.call(context, fields, files); 516 | } 517 | }); 518 | 519 | /** 520 | * Add a hook for adding a completion callback. 521 | * 522 | * @param {Function} callback 523 | * @returns {Form} 524 | * @api public 525 | */ 526 | form.before = function befores(callback, contexts) { 527 | if (form.listeners('end').length) { 528 | form.resume(); // Resume a possible buffered post. 529 | 530 | before = callback; 531 | context = contexts; 532 | 533 | return form; 534 | } 535 | 536 | callback.call(contexts || context, fields, files); 537 | return form; 538 | }; 539 | 540 | return form.parse(this._req); 541 | }); 542 | 543 | // 544 | // !IMPORTANT 545 | // 546 | // These function's & properties should never overridden as we might depend on 547 | // them internally, that's why they are configured with writable: false and 548 | // configurable: false by default. 549 | // 550 | // !IMPORTANT 551 | // 552 | 553 | /** 554 | * Discover pagelets that we're allowed to use. 555 | * 556 | * @returns {Pagelet} fluent interface 557 | * @api private 558 | */ 559 | Pagelet.readable('discover', function discover() { 560 | var req = this._req 561 | , res = this._res 562 | , pagelet = this; 563 | 564 | // 565 | // We need to do an async map/filter of the pagelets, in order to this as 566 | // efficient as possible we're going to use a reduce. 567 | // 568 | async.reduce(this._children, { 569 | disabled: [], 570 | enabled: [] 571 | }, function reduce(memo, children, next) { 572 | children = children.slice(0); 573 | 574 | var child, last; 575 | 576 | async.whilst(function work() { 577 | return children.length && !child; 578 | }, function work(next) { 579 | var Child = children.shift() 580 | , test = new Child({ 581 | bootstrap: pagelet.bootstrap, 582 | bigpipe: pagelet._bigpipe, 583 | res: res, 584 | req: req 585 | }); 586 | 587 | test.conditional(req, children, function conditionally(accepted) { 588 | if (last && last.destroy) last.destroy(); 589 | 590 | if (accepted) child = test; 591 | else last = test; 592 | 593 | next(!!child); 594 | }); 595 | }, function found() { 596 | if (child) memo.enabled.push(child); 597 | else memo.disabled.push(last); 598 | 599 | next(undefined, memo); 600 | }); 601 | }, function discovered(err, children) { 602 | pagelet._disabled = children.disabled; 603 | pagelet._enabled = children.enabled; 604 | 605 | pagelet._enabled.forEach(function initialize(child) { 606 | if ('function' === typeof child.initialize) child.initialize(); 607 | }); 608 | 609 | pagelet.debug('Initialized all allowed pagelets'); 610 | pagelet.emit('discover'); 611 | }); 612 | 613 | return this; 614 | }); 615 | 616 | /** 617 | * Process the pagelet for an async or pipeline based render flow. 618 | * 619 | * @param {String} name Optional name, defaults to pagelet.name. 620 | * @param {Mixed} chunk Content of Pagelet. 621 | * @returns {Bootstrap} Reference to bootstrap Pagelet. 622 | * @api private 623 | */ 624 | Pagelet.readable('write', function write(name, chunk) { 625 | if (!chunk) { 626 | chunk = name; 627 | name = this.name; 628 | } 629 | 630 | this.debug('Queueing data chunk'); 631 | return this.bootstrap.queue(name, this._parent, chunk); 632 | }); 633 | 634 | /** 635 | * Close the connection once all pagelets are sent. 636 | * 637 | * @param {Mixed} chunk Fragment of data. 638 | * @returns {Boolean} Closed the connection. 639 | * @api private 640 | */ 641 | Pagelet.readable('end', function end(chunk) { 642 | var pagelet = this; 643 | 644 | // 645 | // Write data chunk to the queue. 646 | // 647 | if (chunk) this.write(chunk); 648 | 649 | // 650 | // Do not close the connection before all pagelets are send. 651 | // 652 | if (this.bootstrap.length > 0) { 653 | this.debug('Not all pagelets have been written, (%s out of %s)', 654 | this.bootstrap.length, this.length 655 | ); 656 | return false; 657 | } 658 | 659 | // 660 | // Everything is processed, close the connection and clean up references. 661 | // 662 | this.bootstrap.flush(function close(error) { 663 | if (error) return pagelet.capture(error, true); 664 | 665 | pagelet.debug('Closed the connection'); 666 | pagelet._res.end(); 667 | }); 668 | 669 | return true; 670 | }); 671 | 672 | /** 673 | * Set or get the value of the character set, only allows strings. 674 | * 675 | * @type {String} 676 | * @api public 677 | */ 678 | Pagelet.set('charset', function get() { 679 | return this._charset; 680 | }, function set(value) { 681 | if ('string' !== typeof value) return; 682 | return this._charset = value; 683 | }); 684 | 685 | /** 686 | * The Content-Type of the response. This defaults to text/html with a charset 687 | * preset inherited from the charset property. 688 | * 689 | * @type {String} 690 | * @api public 691 | */ 692 | Pagelet.set('contentType', function get() { 693 | return this._contentType +';charset='+ this._charset; 694 | }, function set(value) { 695 | return this._contentType = value; 696 | }); 697 | 698 | /** 699 | * Returns reference to bootstrap Pagelet, which could be the Pagelet itself. 700 | * Allows more chaining and valid bootstrap Pagelet references. 701 | * 702 | * @type {String} 703 | * @public 704 | */ 705 | Pagelet.set('bootstrap', function get() { 706 | return !this._bootstrap && this.name === 'bootstrap' ? this : this._bootstrap || {}; 707 | }, function set(value) { 708 | if (value && value.name === 'bootstrap') return this._bootstrap = value; 709 | }); 710 | 711 | /** 712 | * Checks if we're an active Pagelet or if we still need to a do an check 713 | * against the `if` function. 714 | * 715 | * @type {Boolean} 716 | * @private 717 | */ 718 | Pagelet.set('active', function get() { 719 | return 'function' !== typeof this.if // No conditional check needed. 720 | || this._active !== null && this._active; // Conditional check has been done. 721 | }, function set(value) { 722 | return this._active = !!value; 723 | }); 724 | 725 | /** 726 | * Helper method that proxies to the redirect of the BigPipe instance. 727 | * 728 | * @param {String} path Redirect URI. 729 | * @param {Number} status Optional status code. 730 | * @param {Object} options Optional options, e.g. caching headers. 731 | * @returns {Pagelet} fluent interface. 732 | * @api public 733 | */ 734 | Pagelet.readable('redirect', function redirect(path, status, options) { 735 | this._bigpipe.redirect(this, path, status, options); 736 | return this; 737 | }); 738 | 739 | /** 740 | * Proxy to return the compiled server template from Temper. 741 | * 742 | * @param {String} view Absolute path to the templates location. 743 | * @param {Object} data Used to render the server-side template. 744 | * @return {String} Generated HTML. 745 | * @public 746 | */ 747 | Pagelet.readable('template', function template(view, data) { 748 | if ('string' !== typeof view) { 749 | data = view; 750 | view = this.view; 751 | } 752 | 753 | return this._temper.fetch(view).server(data || {}); 754 | }); 755 | 756 | /** 757 | * Render takes care of all the data merging and `get` invocation. 758 | * 759 | * Options: 760 | * 761 | * - context: Context on which to call `after`, defaults to pagelet. 762 | * - data: stringified object representation to pass to the client. 763 | * - pagelets: Alternate pagelets to be used when this pagelet is not enabled. 764 | * 765 | * @param {Object} options Add post render functionality. 766 | * @param {Function} fn Completion callback. 767 | * @returns {Pagelet} 768 | * @api private 769 | */ 770 | Pagelet.readable('render', function render(options, fn) { 771 | if ('undefined' === typeof fn) { 772 | fn = options; 773 | options = {}; 774 | } 775 | 776 | options = options || {}; 777 | 778 | var framework = this._bigpipe._framework 779 | , compiler = this._bigpipe._compiler 780 | , context = options.context || this 781 | , mode = options.mode || 'async' 782 | , data = options.data || {} 783 | , bigpipe = this._bigpipe 784 | , temper = this._temper 785 | , query = this.query 786 | , pagelet = this 787 | , state = {}; 788 | 789 | /** 790 | * Write the fragmented data. 791 | * 792 | * @param {String} content The content to respond with. 793 | * @returns {Pagelet} 794 | * @api private 795 | */ 796 | function fragment(content) { 797 | var active = pagelet.active; 798 | 799 | if (!active) content = ''; 800 | if (mode === 'sync') return fn.call(context, undefined, content); 801 | 802 | data.id = data.id || pagelet.id; // Pagelet id. 803 | data.path = data.path || pagelet.path; // Reference to the path. 804 | data.mode = data.mode || pagelet.mode; // Pagelet render mode. 805 | data.remove = active ? false : pagelet.remove; // Remove from DOM. 806 | data.parent = pagelet._parent; // Send parent name along. 807 | data.append = pagelet._append; // Content should be appended. 808 | data.remaining = pagelet.bootstrap.length; // Remaining pagelets number. 809 | data.hash = { // Temper md5's for template ref 810 | error: temper.fetch(pagelet.error).hash.client, 811 | client: temper.fetch(pagelet.view).hash.client 812 | }; 813 | 814 | fn.call(context, undefined, framework.get('fragment', { 815 | template: content.replace(//, ''), 816 | name: pagelet.name, 817 | id: pagelet.id, 818 | state: state, 819 | data: data 820 | })); 821 | 822 | return pagelet; 823 | } 824 | 825 | return this.conditional(this._req, options.pagelets, function auth(enabled) { 826 | if (!enabled) return fragment(''); 827 | 828 | // 829 | // Invoke the provided get function and make sure options is an object, from 830 | // which `after` can be called in proper context. 831 | // 832 | pagelet.get(function receive(err, result) { 833 | var view = temper.fetch(pagelet.view).server 834 | , content; 835 | 836 | // 837 | // Add some template defaults. 838 | // 839 | result = result || {}; 840 | if (!('path' in result)) result.path = pagelet.path; 841 | 842 | // 843 | // We've made it this far, but now we have to cross our fingers and HOPE 844 | // that our given template can actually handle the data correctly 845 | // without throwing an error. As the rendering is done synchronously, we 846 | // wrap it in a try/catch statement and hope that an error is thrown 847 | // when the template fails to render the content. If there's an error we 848 | // will process the error template instead. 849 | // 850 | try { 851 | if (err) { 852 | pagelet.debug('Render %s/%s resulted in a error', pagelet.name, pagelet.id, err); 853 | throw err; // Throw so we can capture it again. 854 | } 855 | 856 | content = view(result, { html: true }); 857 | } catch (e) { 858 | if ('production' !== pagelet.env) { 859 | pagelet.debug('Captured rendering error: %s', e.stack); 860 | } 861 | 862 | // 863 | // This is basically fly or die, if the supplied error template throws 864 | // an error while rendering we're basically fucked, your server will 865 | // crash, an angry mob of customers with pitchforks will kick in the 866 | // doors of your office and smear you with peck and feathers for not 867 | // writing a more stable application. 868 | // 869 | if (!pagelet.error) return fn(e); 870 | 871 | content = temper.fetch(pagelet.error).server(pagelet.merge(result, { 872 | reason: 'Failed to render: '+ pagelet.name, 873 | env: pagelet.env, 874 | message: e.message, 875 | stack: e.stack, 876 | error: e 877 | }), { html: true }); 878 | } 879 | 880 | // 881 | // Add queried parts of data, so the client-side script can use it. 882 | // 883 | if ('object' === typeof result && Array.isArray(query) && query.length) { 884 | state = query.reduce(function find(memo, q) { 885 | memo[q] = dot.get(result, q); 886 | return memo; 887 | }, {}); 888 | } 889 | 890 | fragment(content); 891 | }); 892 | }); 893 | }); 894 | 895 | /** 896 | * Authenticate the Pagelet. 897 | * 898 | * @param {Request} req The HTTP request. 899 | * @param {Function} list Array of optional alternate pagelets that take it's place. 900 | * @param {Function} fn The authorized callback. 901 | * @returns {Pagelet} 902 | * @api private 903 | */ 904 | Pagelet.readable('conditional', function conditional(req, list, fn) { 905 | var pagelet = this; 906 | 907 | if ('function' !== typeof fn) { 908 | fn = list; 909 | list = []; 910 | } 911 | 912 | /** 913 | * Callback for the `pagelet.if` function to see if we're enabled or disabled. 914 | * Use cached value in _active to prevent the same Pagelet being authorized 915 | * multiple times for the same request. 916 | * 917 | * @param {Boolean} value Are we enabled or disabled. 918 | * @api private 919 | */ 920 | function enabled(value) { 921 | fn.call(pagelet, pagelet.active = value || false); 922 | } 923 | 924 | if ('boolean' === typeof pagelet._active) { 925 | fn(pagelet.active); 926 | } else if ('function' !== typeof this.if) { 927 | fn(pagelet.active = true); 928 | } else { 929 | if (pagelet.if.length === 2) pagelet.if(req, enabled); 930 | else pagelet.if(req, list, enabled); 931 | } 932 | 933 | return pagelet; 934 | }); 935 | 936 | /** 937 | * Destroy the pagelet and remove all the back references so it can be safely 938 | * garbage collected. 939 | * 940 | * @api public 941 | */ 942 | Pagelet.readable('destroy', destroy([ 943 | '_temper', '_bigpipe', '_enabled', '_disabled', '_children' 944 | ], { 945 | after: 'removeAllListeners' 946 | })); 947 | 948 | 949 | /** 950 | * Expose the Pagelet on the exports and parse our the directory. This ensures 951 | * that we can properly resolve all relative assets: 952 | * 953 | * ```js 954 | * Pagelet.extend({ 955 | * .. 956 | * }).on(module); 957 | * ``` 958 | * 959 | * The use of this function is for convenience and optional. Developers can 960 | * choose to provide absolute paths to files. 961 | * 962 | * @param {Module} module The reference to the module object. 963 | * @returns {Pagelet} 964 | * @api public 965 | */ 966 | Pagelet.on = function on(module) { 967 | var prototype = this.prototype 968 | , dir = prototype.directory = path.dirname(module.filename); 969 | 970 | // 971 | // Resolve the view and error templates to ensure 972 | // absolute paths are provided to Temper. 973 | // 974 | if (prototype.error) prototype.error = path.resolve(dir, prototype.error); 975 | if (prototype.view) prototype.view = path.resolve(dir, prototype.view); 976 | 977 | return module.exports = this; 978 | }; 979 | 980 | /** 981 | * Discover all pagelets recursive. Fabricate will create constructable 982 | * instances from the provided value of prototype.pagelets. 983 | * 984 | * @param {String} parent Reference to the parent pagelet name. 985 | * @return {Array} collection of pagelets instances. 986 | * @api public 987 | */ 988 | Pagelet.children = function children(parent, stack) { 989 | var pagelets = this.prototype.pagelets 990 | , log = debug('pagelet:'+ parent); 991 | 992 | stack = stack || []; 993 | return fabricate(pagelets, { 994 | source: this.prototype.directory, 995 | recursive: 'string' === typeof pagelets 996 | }).reduce(function each(stack, Pagelet) { 997 | // 998 | // Pagelet could be conditional, simple crawl this function 999 | // again to get the children of each conditional. 1000 | // 1001 | if (Array.isArray(Pagelet)) return Pagelet.reduce(each, []); 1002 | 1003 | var name = Pagelet.prototype.name; 1004 | log('Recursive discovery of child pagelet %s', name); 1005 | 1006 | // 1007 | // We need to extend the pagelet if it already has a _parent name reference 1008 | // or will accidentally override it. This can happen when you extend a parent 1009 | // pagelet with children and alter the parent's name. The extended parent and 1010 | // regular parent still point to the same child pagelets. So when we try to 1011 | // set the proper parent, these pagelets will override the _parent property 1012 | // unless we create a new fresh instance and set it on that instead. 1013 | // 1014 | if (Pagelet.prototype._parent && name !== parent) { 1015 | Pagelet = Pagelet.extend(); 1016 | } 1017 | 1018 | Pagelet.prototype._parent = parent; 1019 | return Pagelet.children(name, stack.concat(Pagelet)); 1020 | }, stack); 1021 | }; 1022 | 1023 | /** 1024 | * Optimize the prototypes of Pagelets to reduce work when we're actually 1025 | * serving the requests via BigPipe. 1026 | * 1027 | * Options: 1028 | * - temper: A custom temper instance we want to use to compile the templates. 1029 | * 1030 | * @param {Object} options Optimization configuration. 1031 | * @param {Function} next Completion callback for async execution. 1032 | * @api public 1033 | */ 1034 | Pagelet.optimize = function optimize(options, done) { 1035 | if ('function' === typeof options) { 1036 | done = options; 1037 | options = {}; 1038 | } 1039 | 1040 | var stack = [] 1041 | , Pagelet = this 1042 | , bigpipe = options.bigpipe || {} 1043 | , transform = options.transform || {} 1044 | , temper = options.temper || bigpipe._temper 1045 | , before, after; 1046 | 1047 | // 1048 | // Check if before listener is found. Add before emit to the stack. 1049 | // This async function will be called before optimize. 1050 | // 1051 | if (bigpipe._events && 'transform:pagelet:before' in bigpipe._events) { 1052 | before = bigpipe._events['transform:pagelet:before'].length || 1; 1053 | 1054 | stack.push(function run(next) { 1055 | var n = 0; 1056 | 1057 | transform.before(Pagelet, function ran(error, Pagelet) { 1058 | if (error || ++n === before) return next(error, Pagelet); 1059 | }); 1060 | }); 1061 | } 1062 | 1063 | // 1064 | // If transform.before was not pushed on the stack, optimizer needs 1065 | // to called with a reference to Pagelet. 1066 | // 1067 | stack.push(!stack.length ? async.apply(optimizer, Pagelet) : optimizer); 1068 | 1069 | // 1070 | // Check if after listener is found. Add after emit to the stack. 1071 | // This async function will be called after optimize. 1072 | // 1073 | if (bigpipe._events && 'transform:pagelet:after' in bigpipe._events) { 1074 | after = bigpipe._events['transform:pagelet:after'].length || 1; 1075 | 1076 | stack.push(function run(Pagelet, next) { 1077 | var n = 0; 1078 | 1079 | transform.after(Pagelet, function ran(error, Pagelet) { 1080 | if (error || ++n === after) return next(error, Pagelet); 1081 | }); 1082 | }); 1083 | } 1084 | 1085 | // 1086 | // Run the stack in series. This ensures that before hooks are run 1087 | // prior to optimizing and after hooks are ran post optimizing. 1088 | // 1089 | async.waterfall(stack, done); 1090 | 1091 | /** 1092 | * Optimize the pagelet. This function is called by default as part of 1093 | * the async stack. 1094 | * 1095 | * @param {Function} next Completion callback 1096 | * @api private 1097 | */ 1098 | function optimizer(Pagelet, next) { 1099 | var prototype = Pagelet.prototype 1100 | , method = prototype.method 1101 | , status = prototype.status 1102 | , router = prototype.path 1103 | , name = prototype.name 1104 | , view = prototype.view 1105 | , log = debug('pagelet:'+ name); 1106 | 1107 | // 1108 | // Generate a unique ID used for real time connection lookups. 1109 | // 1110 | prototype.id = options.id || [0, 1, 1, 1].map(generator).join('-'); 1111 | 1112 | // 1113 | // Parse the methods to an array of accepted HTTP methods. We'll only accept 1114 | // these requests and should deny every other possible method. 1115 | // 1116 | log('Optimizing pagelet'); 1117 | if (!Array.isArray(method)) method = method.split(/[\s\,]+?/); 1118 | Pagelet.method = method.filter(Boolean).map(function transformation(method) { 1119 | return method.toUpperCase(); 1120 | }); 1121 | 1122 | // 1123 | // Add the actual HTTP route and available HTTP methods. 1124 | // 1125 | if (router) { 1126 | log('Instantiating router for path %s', router); 1127 | Pagelet.router = new Route(router); 1128 | } 1129 | 1130 | // 1131 | // Prefetch the template if a view is available. The view property is 1132 | // mandatory for all pagelets except the bootstrap Pagelet or if the 1133 | // Pagelet is just doing a redirect. We can resolve this edge case by 1134 | // checking if statusCode is in the 300~ range. 1135 | // 1136 | if (!view && name !== 'bootstrap' && !(status >= 300 && status < 400)) return next( 1137 | new Error('The '+ name +' pagelet should have a .view property.') 1138 | ); 1139 | 1140 | // 1141 | // Resolve the view to ensure the path is correct and prefetch 1142 | // the template through Temper. 1143 | // 1144 | if (view) { 1145 | prototype.view = view = path.resolve(prototype.directory, view); 1146 | temper.prefetch(view, prototype.engine); 1147 | } 1148 | 1149 | // 1150 | // Ensure we have a custom error pagelet when we fail to render this fragment. 1151 | // 1152 | if (prototype.error) { 1153 | temper.prefetch(prototype.error, path.extname(prototype.error).slice(1)); 1154 | } 1155 | 1156 | // 1157 | // Map all dependencies to an absolute path or URL. 1158 | // 1159 | helpers.resolve(Pagelet, ['css', 'js', 'dependencies']); 1160 | 1161 | // 1162 | // Find all child pagelets and optimize the found children. 1163 | // 1164 | async.map(Pagelet.children(name), function map(Child, step) { 1165 | if (Array.isArray(Child)) return async.map(Child, map, step); 1166 | 1167 | Child.optimize({ 1168 | temper: temper, 1169 | bigpipe: bigpipe, 1170 | transform: { 1171 | before: bigpipe.emits && bigpipe.emits('transform:pagelet:before'), 1172 | after: bigpipe.emits && bigpipe.emits('transform:pagelet:after') 1173 | } 1174 | }, step); 1175 | }, function optimized(error, children) { 1176 | log('optimized all %d child pagelets', children.length); 1177 | 1178 | if (error) return next(error); 1179 | 1180 | // 1181 | // Store the optimized children on the prototype, wrapping the Pagelet 1182 | // in an array makes it a lot easier to work with conditional Pagelets. 1183 | // 1184 | prototype._children = children.map(function map(Pagelet) { 1185 | return Array.isArray(Pagelet) ? Pagelet : [Pagelet]; 1186 | }); 1187 | 1188 | // 1189 | // Always return a reference to the parent Pagelet. 1190 | // Otherwise the stack of parents would be infested 1191 | // with children returned by this async.map. 1192 | // 1193 | next(null, Pagelet); 1194 | }); 1195 | } 1196 | }; 1197 | 1198 | // 1199 | // Expose the pagelet. 1200 | // 1201 | module.exports = Pagelet; 1202 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pagelet", 3 | "version": "0.9.3", 4 | "description": "pagelet", 5 | "main": "index.js", 6 | "scripts": { 7 | "100%": "istanbul check-coverage --statements 100 --functions 100 --lines 100 --branches 100", 8 | "test": "mocha $(find test -name '*.test.js')", 9 | "watch": "mocha --watch $(find test -name '*.test.js')", 10 | "coverage": "istanbul cover ./node_modules/.bin/_mocha -- $(find test -name '*.test.js')", 11 | "test-travis": "istanbul cover node_modules/.bin/_mocha --report lcovonly -- $(find test -name '*.test.js')" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/bigpipe/pagelet" 16 | }, 17 | "keywords": [ 18 | "pagelet", 19 | "bigpipe" 20 | ], 21 | "author": "Arnout Kazemier", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/bigpipe/pagelet/issues" 25 | }, 26 | "homepage": "http://bigpipe.io", 27 | "dependencies": { 28 | "async": "0.9.x", 29 | "demolish": "1.0.x", 30 | "diagnostics": "0.0.x", 31 | "dot-component": "0.1.x", 32 | "eventemitter3": "0.1.x", 33 | "fabricator": "0.5.x", 34 | "formidable": "1.0.x", 35 | "fusing": "1.0.x", 36 | "routable": "0.0.x", 37 | "temper": "0.3.x" 38 | }, 39 | "devDependencies": { 40 | "assume": "1.1.x", 41 | "bigpipe": "bigpipe/bigpipe", 42 | "istanbul": "0.3.x", 43 | "mocha": "2.2.x", 44 | "pre-commit": "1.0.x", 45 | "react": "0.13.x", 46 | "react-jsx": "0.13.x" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pagelet.fragment: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /test/common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var EventEmitter = require('events').EventEmitter; 4 | 5 | // 6 | // Request stub 7 | // 8 | function Request(url, method) { 9 | this.headers = {}; 10 | this.url = url || ''; 11 | this.uri = require('url').parse(this.url, true); 12 | this.query = this.uri.query || {}; 13 | this.method = method || 'GET'; 14 | } 15 | 16 | require('util').inherits(Request, EventEmitter); 17 | 18 | // 19 | // Response stub 20 | // 21 | function Response() { 22 | this.setHeader = this.write = this.end = this.once = function noop() {}; 23 | } 24 | 25 | // 26 | // Expose the helpers. 27 | // 28 | exports.Request = Request; 29 | exports.Response = Response; -------------------------------------------------------------------------------- /test/fixtures/error.html: -------------------------------------------------------------------------------- 1 |

Custom error template

2 | 3 |

You failed!

-------------------------------------------------------------------------------- /test/fixtures/style.css: -------------------------------------------------------------------------------- 1 | * { color: red } 2 | -------------------------------------------------------------------------------- /test/fixtures/view.html: -------------------------------------------------------------------------------- 1 |

Some {test} fixture

-------------------------------------------------------------------------------- /test/fixtures/view.jsx: -------------------------------------------------------------------------------- 1 | /** @jsx React.DOM */ 2 | 3 |

4 | -------------------------------------------------------------------------------- /test/helpers.test.js: -------------------------------------------------------------------------------- 1 | describe('Helpers', function () { 2 | 'use strict'; 3 | 4 | var Pagelet = require('../').extend({ name: 'test' }) 5 | , custom = '/unexisting/absolute/path/to/prepend' 6 | , helpers = require('../helpers') 7 | , assume = require('assume'); 8 | 9 | describe('.resolve', function () { 10 | var pagelet, P; 11 | 12 | beforeEach(function () { 13 | P = Pagelet.extend({ 14 | directory: __dirname, 15 | view: 'fixtures/view.html', 16 | css: 'fixtures/style.css', 17 | js: '//cdnjs.cloudflare.com/ajax/libs/d3/3.4.8/d3.min.js', 18 | dependencies: [ 19 | 'http://code.jquery.com/jquery-2.0.0.js', 20 | 'fixtures/custom.js' 21 | ] 22 | }); 23 | 24 | pagelet = new P; 25 | }); 26 | 27 | afterEach(function each() { 28 | pagelet = null; 29 | }); 30 | 31 | it('is a function', function () { 32 | assume(helpers.resolve).to.be.a('function'); 33 | }); 34 | 35 | it('will resolve provided property on prototype', function () { 36 | var result = helpers.resolve(P, 'css'); 37 | 38 | assume(result).to.equal(P); 39 | assume(P.prototype.css).to.be.an('array'); 40 | assume(P.prototype.css.length).to.equal(1); 41 | assume(P.prototype.css[0]).to.equal(__dirname + '/fixtures/style.css'); 42 | }); 43 | 44 | it('can resolve multiple properties at once', function () { 45 | helpers.resolve(P, ['css', 'js']); 46 | 47 | assume(P.prototype.css).to.be.an('array'); 48 | assume(P.prototype.js).to.be.an('array'); 49 | assume(P.prototype.css.length).to.equal(1); 50 | assume(P.prototype.js.length).to.equal(1); 51 | }); 52 | 53 | it('can be provided with a custom source directory', function () { 54 | helpers.resolve(P, 'css', custom); 55 | 56 | assume(P.prototype.css[0]).to.equal(custom + '/fixtures/style.css'); 57 | }); 58 | 59 | it('only resolves local files', function () { 60 | helpers.resolve(P, 'js', custom); 61 | 62 | assume(P.prototype.js[0]).to.not.include(custom); 63 | assume(P.prototype.js[0]).to.equal('//cdnjs.cloudflare.com/ajax/libs/d3/3.4.8/d3.min.js'); 64 | }); 65 | 66 | it('can handle property values that are already an array', function () { 67 | helpers.resolve(P, 'dependencies', custom); 68 | 69 | assume(P.prototype.dependencies.length).to.equal(2); 70 | assume(P.prototype.dependencies[0]).to.not.include(custom); 71 | assume(P.prototype.dependencies[0]).to.equal('http://code.jquery.com/jquery-2.0.0.js'); 72 | assume(P.prototype.dependencies[1]).to.equal(custom + '/fixtures/custom.js'); 73 | }); 74 | 75 | it('removes undefined values from the array before processing', function () { 76 | var Undef = P.extend({ 77 | dependencies: P.prototype.dependencies.concat( 78 | undefined 79 | ) 80 | }); 81 | 82 | assume(Undef.prototype.dependencies.length).to.equal(3); 83 | 84 | helpers.resolve(Undef, 'dependencies', custom); 85 | assume(Undef.prototype.dependencies.length).to.equal(2); 86 | assume(Undef.prototype.dependencies).to.not.include(undefined); 87 | }); 88 | 89 | it('can be overriden', function () { 90 | P.resolve = function () { 91 | throw new Error('fucked'); 92 | }; 93 | 94 | P.on({}); 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | --ui bdd 3 | -------------------------------------------------------------------------------- /test/pagelet.test.js: -------------------------------------------------------------------------------- 1 | describe('Pagelet', function () { 2 | 'use strict'; 3 | 4 | var server = require('http').createServer() 5 | , BigPipe = require('bigpipe') 6 | , common = require('./common') 7 | , Temper = require('temper') 8 | , assume = require('assume') 9 | , Response = common.Response 10 | , Request = common.Request 11 | , Pagelet = require('../') 12 | , React = require('react') 13 | , pagelet, P; 14 | 15 | // 16 | // A lazy mans temper, we just want ignore all temper actions sometimes 17 | // because our pagelet is not exported using `.on(module)` 18 | // 19 | var temper = new Temper 20 | , bigpipe = new BigPipe(server); 21 | 22 | // 23 | // Stub for no operation callbacks. 24 | // 25 | function noop() {} 26 | 27 | beforeEach(function () { 28 | P = Pagelet.extend({ 29 | directory: __dirname, 30 | error: 'fixtures/error.html', 31 | view: 'fixtures/view.html', 32 | css: 'fixtures/style.css', 33 | js: '//cdnjs.cloudflare.com/ajax/libs/d3/3.4.8/d3.min.js', 34 | dependencies: [ 35 | 'http://code.jquery.com/jquery-2.0.0.js', 36 | 'fixtures/custom.js' 37 | ] 38 | }); 39 | 40 | pagelet = new P({ temper: temper }); 41 | }); 42 | 43 | afterEach(function each() { 44 | pagelet = null; 45 | }); 46 | 47 | it('rendering is asynchronous', function (done) { 48 | pagelet.get(pagelet.emits('called')); 49 | 50 | // Listening only till after the event is potentially emitted, will ensure 51 | // callbacks are called asynchronously by pagelet.render. 52 | pagelet.on('called', done); 53 | }); 54 | 55 | it('can have reference to temper', function () { 56 | pagelet = new P({ temper: temper }); 57 | var property = Object.getOwnPropertyDescriptor(pagelet, '_temper'); 58 | 59 | assume(pagelet._temper).to.be.an('object'); 60 | assume(property.writable).to.equal(true); 61 | assume(property.enumerable).to.equal(false); 62 | assume(property.configurable).to.equal(true); 63 | }); 64 | 65 | it('can have reference to bigpipe instance', function () { 66 | pagelet = new P({ bigpipe: bigpipe }); 67 | var property = Object.getOwnPropertyDescriptor(pagelet, '_bigpipe'); 68 | 69 | assume(pagelet._bigpipe).to.be.an('object'); 70 | assume(pagelet._bigpipe).to.be.instanceof(BigPipe); 71 | assume(property.writable).to.equal(true); 72 | assume(property.enumerable).to.equal(false); 73 | assume(property.configurable).to.equal(true); 74 | }); 75 | 76 | describe('#on', function () { 77 | it('is a function', function () { 78 | assume(Pagelet.on).is.a('function'); 79 | assume(Pagelet.on.length).to.equal(1); 80 | }); 81 | 82 | it('sets the directory property to dirname', function () { 83 | var pagelet = Pagelet.extend({}); 84 | assume(pagelet.prototype.directory).to.equal(''); 85 | 86 | pagelet.prototype.directory = 'foo'; 87 | assume(pagelet.prototype.directory).to.equal('foo'); 88 | 89 | pagelet.on(module); 90 | 91 | assume(pagelet.prototype.directory).to.be.a('string'); 92 | assume(pagelet.prototype.directory).to.equal(__dirname); 93 | }); 94 | 95 | it('resolves the view', function () { 96 | assume(P.prototype.view).to.equal('fixtures/view.html'); 97 | 98 | P.on(module); 99 | assume(P.prototype.view).to.equal(__dirname +'/fixtures/view.html'); 100 | }); 101 | 102 | it('resolves the `error` view', function () { 103 | assume(P.prototype.error).to.equal('fixtures/error.html'); 104 | 105 | P.on(module); 106 | assume(P.prototype.error).to.equal(__dirname +'/fixtures/error.html'); 107 | }); 108 | }); 109 | 110 | describe('#destroy', function () { 111 | it('is a function', function () { 112 | assume(pagelet.destroy).to.be.a('function'); 113 | assume(pagelet.destroy.length).to.equal(0); 114 | }); 115 | 116 | it('cleans object references from the Pagelet instance', function () { 117 | var local = new Pagelet({ temper: temper, bigpipe: bigpipe }); 118 | local.on('test', noop); 119 | 120 | local.destroy(); 121 | assume(local).to.have.property('_temper', null); 122 | assume(local).to.have.property('_bigpipe', null); 123 | assume(local).to.have.property('_children', null); 124 | assume(local).to.have.property('_events', null); 125 | }); 126 | }); 127 | 128 | describe('#discover', function () { 129 | it('emits discover and returns immediatly if the parent pagelet has no children', function (done) { 130 | pagelet.once('discover', done); 131 | pagelet.discover(); 132 | }); 133 | 134 | /* Disabled for now, might return before 1.0.0 135 | it('initializes pagelets by allocating from the Pagelet.freelist', function (done) { 136 | var Hero = require(__dirname + '/fixtures/pagelets/hero').optimize(app.temper) 137 | , Faq = require(__dirname + '/fixtures/pages/faq').extend({ pagelets: [ Hero ] }) 138 | , pageletFreelist = sinon.spy(Hero.freelist, 'alloc') 139 | , faq = new Faq(app); 140 | 141 | faq.once('discover', function () { 142 | assume(pageletFreelist).to.be.calledOnce; 143 | done(); 144 | }); 145 | 146 | faq.discover(); 147 | });*/ 148 | }); 149 | 150 | describe('#length', function () { 151 | it('is a getter', function () { 152 | var props = Object.getOwnPropertyDescriptor(Pagelet.prototype, 'length'); 153 | 154 | assume(Pagelet.prototype).to.have.property('length'); 155 | assume(props).to.have.property('get'); 156 | assume(props.get).to.be.a('function'); 157 | 158 | assume(props).to.have.property('set', void 0); 159 | assume(props).to.have.property('enumerable', false); 160 | assume(props).to.have.property('configurable', false); 161 | }); 162 | 163 | it('returns the childrens length', function () { 164 | pagelet._children = [ 1, 2, 3 ]; 165 | assume(pagelet.length).to.equal(3); 166 | }); 167 | }); 168 | 169 | describe('#template', function () { 170 | it('is a function', function () { 171 | assume(Pagelet.prototype.template).to.be.a('function'); 172 | assume(P.prototype.template).to.be.a('function'); 173 | assume(Pagelet.prototype.template).to.equal(P.prototype.template); 174 | assume(pagelet.template).to.equal(P.prototype.template); 175 | }); 176 | 177 | it('returns compiled server template from Temper by path', function () { 178 | var result = pagelet.template(__dirname + '/fixtures/view.html', { 179 | test: 'data' 180 | }); 181 | 182 | assume(result).to.be.a('string'); 183 | assume(result).to.equal('

Some data fixture

'); 184 | }); 185 | 186 | it('returns compiled server React template for jsx templates', function () { 187 | var result = pagelet.template(__dirname + '/fixtures/view.jsx', { 188 | Component: React.createClass({ 189 | render: function () { 190 | return ( 191 | React.createElement('span', null, 'some text') 192 | ); 193 | } 194 | }), 195 | test: 'data' 196 | }); 197 | 198 | assume(result).to.be.a('object'); 199 | assume(React.isValidElement(result)).is.true(); 200 | }); 201 | 202 | it('defaults to the pagelets view if no path is provided', function() { 203 | var result = new (P.extend().on(module))({ temper: temper }).template({ 204 | test: 'data' 205 | }); 206 | 207 | assume(result).to.be.a('string'); 208 | assume(result).to.equal('

Some data fixture

'); 209 | }); 210 | 211 | it('provides empty object as fallback for data', function() { 212 | var result = new (P.extend().on(module))({ temper: temper }).template(); 213 | 214 | assume(result).to.be.a('string'); 215 | assume(result).to.equal('

Some {test} fixture

'); 216 | }); 217 | }); 218 | 219 | describe('#contentType', function () { 220 | it('is a getter', function () { 221 | var props = Object.getOwnPropertyDescriptor(Pagelet.prototype, 'contentType'); 222 | 223 | assume(Pagelet.prototype).to.have.property('contentType'); 224 | assume(props).to.have.property('get'); 225 | assume(props.get).to.be.a('function'); 226 | 227 | assume(props).to.have.property('enumerable', false); 228 | assume(props).to.have.property('configurable', false); 229 | }); 230 | 231 | it('is a setter', function () { 232 | var props = Object.getOwnPropertyDescriptor(Pagelet.prototype, 'contentType'); 233 | 234 | assume(Pagelet.prototype).to.have.property('contentType'); 235 | assume(props).to.have.property('set'); 236 | assume(props.get).to.be.a('function'); 237 | 238 | assume(props).to.have.property('enumerable', false); 239 | assume(props).to.have.property('configurable', false); 240 | }); 241 | 242 | it('sets the Content-Type', function () { 243 | pagelet.contentType = 'application/test'; 244 | assume(pagelet._contentType).to.equal('application/test'); 245 | }); 246 | 247 | it('returns the Content-Type of the pagelet appended with the charset', function () { 248 | assume(pagelet.contentType).to.equal('text/html;charset=UTF-8'); 249 | 250 | pagelet._contentType = 'application/test'; 251 | assume(pagelet.contentType).to.equal('application/test;charset=UTF-8'); 252 | 253 | pagelet._charset = 'UTF-7'; 254 | assume(pagelet.contentType).to.equal('application/test;charset=UTF-7'); 255 | }); 256 | }); 257 | 258 | describe('#bootstrap', function () { 259 | it('is a getter', function () { 260 | var props = Object.getOwnPropertyDescriptor(Pagelet.prototype, 'bootstrap'); 261 | 262 | assume(Pagelet.prototype).to.have.property('bootstrap'); 263 | assume(props).to.have.property('get'); 264 | assume(props.get).to.be.a('function'); 265 | 266 | assume(props).to.have.property('enumerable', false); 267 | assume(props).to.have.property('configurable', false); 268 | }); 269 | 270 | it('is a setter', function () { 271 | var props = Object.getOwnPropertyDescriptor(Pagelet.prototype, 'bootstrap'); 272 | 273 | assume(Pagelet.prototype).to.have.property('bootstrap'); 274 | assume(props).to.have.property('set'); 275 | assume(props.get).to.be.a('function'); 276 | 277 | assume(props).to.have.property('enumerable', false); 278 | assume(props).to.have.property('configurable', false); 279 | }); 280 | 281 | it('sets a reference to a bootstrap pagelet', function () { 282 | var bootstrap = new (Pagelet.extend({ name: 'bootstrap' })); 283 | 284 | pagelet.bootstrap = bootstrap; 285 | assume(pagelet._bootstrap).to.equal(bootstrap); 286 | }); 287 | 288 | it('only accepts objects that look like bootstrap pagelets', function () { 289 | pagelet.bootstrap = 'will not be set'; 290 | assume(pagelet._bootstrap).to.equal(void 0); 291 | 292 | pagelet.bootstrap = { name: 'bootstrap', test: 'will be set' }; 293 | assume(pagelet._bootstrap).to.have.property('test', 'will be set'); 294 | }); 295 | 296 | it('returns a reference to the bootstrap pagelet or empty object', function () { 297 | assume(Object.keys(pagelet.bootstrap).length).to.equal(0); 298 | assume(pagelet.bootstrap.name).to.equal(void 0); 299 | 300 | var bootstrap = new (Pagelet.extend({ name: 'bootstrap' })); 301 | 302 | pagelet.bootstrap = bootstrap; 303 | assume(pagelet.bootstrap).to.equal(bootstrap); 304 | }); 305 | 306 | it('returns a reference to self if it is a boostrap pagelet', function () { 307 | var bootstrap = new (Pagelet.extend({ name: 'bootstrap' })); 308 | 309 | assume(bootstrap.bootstrap).to.equal(bootstrap); 310 | }); 311 | }); 312 | 313 | describe('#active', function () { 314 | it('is a getter', function () { 315 | var props = Object.getOwnPropertyDescriptor(Pagelet.prototype, 'active'); 316 | 317 | assume(Pagelet.prototype).to.have.property('active'); 318 | assume(props).to.have.property('get'); 319 | assume(props.get).to.be.a('function'); 320 | 321 | assume(props).to.have.property('enumerable', false); 322 | assume(props).to.have.property('configurable', false); 323 | }); 324 | 325 | it('is a setter', function () { 326 | var props = Object.getOwnPropertyDescriptor(Pagelet.prototype, 'active'); 327 | 328 | assume(Pagelet.prototype).to.have.property('active'); 329 | assume(props).to.have.property('set'); 330 | assume(props.get).to.be.a('function'); 331 | 332 | assume(props).to.have.property('enumerable', false); 333 | assume(props).to.have.property('configurable', false); 334 | }); 335 | 336 | it('sets the provided value to _active as boolean', function () { 337 | pagelet.active = 'true'; 338 | assume(pagelet._active).to.equal(true); 339 | 340 | pagelet.active = false; 341 | assume(pagelet._active).to.equal(false); 342 | }); 343 | 344 | it('returns true if no conditional method is available', function () { 345 | assume(pagelet.active).to.equal(true); 346 | 347 | pagelet._active = false; 348 | assume(pagelet.active).to.equal(true); 349 | }); 350 | 351 | it('returns the boolean value of _active if a conditional method is available', function () { 352 | var Conditional = P.extend({ if: noop }) 353 | , conditional = new Conditional; 354 | 355 | conditional._active = true; 356 | assume(conditional.active).to.equal(true); 357 | 358 | conditional._active = null; 359 | assume(conditional.active).to.equal(false); 360 | 361 | conditional._active = false; 362 | assume(conditional.active).to.equal(false); 363 | }); 364 | }); 365 | 366 | describe('#conditional', function () { 367 | it('is a function', function () { 368 | assume(pagelet.conditional).to.be.a('function'); 369 | assume(pagelet.conditional.length).to.equal(3); 370 | }); 371 | 372 | it('has an optional list argument for alternate pagelets', function (done) { 373 | pagelet.conditional({}, function (authorized) { 374 | assume(authorized).to.equal(true); 375 | done(); 376 | }); 377 | }); 378 | 379 | it('will use cached boolean value of authenticate', function (done) { 380 | var Conditional = P.extend({ 381 | if: function stubAuth(req, enabled) { 382 | assume(enabled).to.be.a('function'); 383 | enabled(req.test === 'stubbed req'); 384 | } 385 | }), conditional; 386 | 387 | conditional = new Conditional; 388 | conditional._active = false; 389 | 390 | conditional.conditional({}, function (authorized) { 391 | assume(authorized).to.equal(false); 392 | 393 | conditional._active = 'invalid boolean'; 394 | conditional.conditional({}, function (authorized) { 395 | assume(authorized).to.equal(false); 396 | done(); 397 | }); 398 | }); 399 | }); 400 | 401 | it('will authorize if no authorization method is provided', function (done) { 402 | pagelet.conditional({}, [], function (authorized) { 403 | assume(authorized).to.equal(true); 404 | assume(pagelet._active).to.equal(true); 405 | done(); 406 | }); 407 | }); 408 | 409 | it('will call authorization method without conditional pagelets', function (done) { 410 | var Conditional = P.extend({ 411 | if: function stubAuth(req, enabled) { 412 | assume(enabled).to.be.a('function'); 413 | enabled(req.test === 'stubbed req'); 414 | } 415 | }); 416 | 417 | new Conditional().conditional({ test: 'stubbed req' }, function (auth) { 418 | assume(auth).to.equal(true); 419 | done(); 420 | }); 421 | }); 422 | 423 | it('will call authorization method with conditional pagelets', function (done) { 424 | var Conditional = P.extend({ 425 | if: function stubAuth(req, list, enabled) { 426 | assume(list).to.be.an('array'); 427 | assume(list.length).to.equal(1); 428 | assume(list[0]).to.be.instanceof(Pagelet); 429 | assume(enabled).to.be.a('function'); 430 | enabled(req.test !== 'stubbed req'); 431 | } 432 | }); 433 | 434 | new Conditional().conditional({ test: 'stubbed req' }, [pagelet], function (auth) { 435 | assume(auth).to.equal(false); 436 | done(); 437 | }); 438 | }); 439 | 440 | it('will default to not authorized if no value is provided to the callback', function (done) { 441 | var Conditional = P.extend({ 442 | if: function stubAuth(req, list, enabled) { 443 | assume(list).to.be.an('array'); 444 | assume(list.length).to.equal(0); 445 | assume(enabled).to.be.a('function'); 446 | enabled(); 447 | } 448 | }); 449 | 450 | new Conditional().conditional({ test: 'stubbed req' }, function (auth) { 451 | assume(auth).to.equal(false); 452 | done(); 453 | }); 454 | }); 455 | }); 456 | 457 | describe('#redirect', function () { 458 | it('is a function', function () { 459 | assume(pagelet.redirect).to.be.a('function'); 460 | assume(pagelet.redirect.length).to.equal(3); 461 | }); 462 | 463 | it('proxies calls to the bigpipe instance', function (done) { 464 | var CustomPipe = BigPipe.extend({ 465 | redirect: function redirect(ref, path, code, options) { 466 | assume(ref).to.be.instanceof(Pagelet); 467 | assume(ref).to.equal(pagelet); 468 | assume(path).to.equal('/test'); 469 | assume(code).to.equal(404); 470 | assume(options).to.have.property('cache', false); 471 | 472 | done(); 473 | } 474 | }); 475 | 476 | pagelet = new P({ bigpipe: new CustomPipe(server) }); 477 | pagelet.redirect('/test', 404, { 478 | cache: false 479 | }); 480 | }); 481 | 482 | it('returns a reference to the pagelet', function () { 483 | pagelet = new P({ bigpipe: bigpipe }); 484 | pagelet._res = new Response; 485 | assume(pagelet.redirect('/')).to.equal(pagelet); 486 | }) 487 | }); 488 | 489 | describe('#children', function () { 490 | it('is a function', function () { 491 | assume(Pagelet.children).to.be.a('function'); 492 | assume(P.children).to.be.a('function'); 493 | assume(Pagelet.children).to.equal(P.children); 494 | }); 495 | 496 | it('returns an array', function () { 497 | var one = P.children() 498 | , recur = P.extend({ 499 | pagelets: { 500 | child: P.extend({ name: 'child' }) 501 | } 502 | }).children('this one'); 503 | 504 | assume(one).to.be.an('array'); 505 | assume(one.length).to.equal(0); 506 | 507 | assume(recur).to.be.an('array'); 508 | assume(recur.length).to.equal(1); 509 | }); 510 | 511 | it('will only return children of the pagelet', function () { 512 | var single = P.children(); 513 | 514 | assume(single).to.be.an('array'); 515 | assume(single.length).to.equal(0); 516 | }); 517 | 518 | it('does recursive pagelet discovery', function () { 519 | var recur = P.extend({ 520 | pagelets: { 521 | child: P.extend({ 522 | name: 'child' , 523 | pagelets: { 524 | another: P.extend({ name: 'another' }) 525 | } 526 | }), 527 | } 528 | }).children('multiple'); 529 | 530 | assume(recur).is.an('array'); 531 | assume(recur.length).to.equal(2); 532 | assume(recur[0].prototype.name).to.equal('child'); 533 | assume(recur[1].prototype.name).to.equal('another'); 534 | }); 535 | 536 | it('sets the pagelets parent name on `_parent`', function () { 537 | var recur = P.extend({ 538 | pagelets: { 539 | child: P.extend({ 540 | name: 'child' 541 | }) 542 | } 543 | }).children('parental'); 544 | 545 | assume(recur[0].prototype._parent).to.equal('parental'); 546 | }); 547 | }); 548 | 549 | describe('#optimize', function () { 550 | it('should prepare an async call stack'); 551 | it('should provide optimizer with Pagelet reference if no transform:before event'); 552 | }); 553 | }); 554 | --------------------------------------------------------------------------------