├── .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: '
{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 |You failed!
-------------------------------------------------------------------------------- /test/fixtures/style.css: -------------------------------------------------------------------------------- 1 | * { color: red } 2 | -------------------------------------------------------------------------------- /test/fixtures/view.html: -------------------------------------------------------------------------------- 1 |