├── test
├── fixtures
│ ├── style.css
│ ├── view.html
│ ├── error.html
│ └── view.jsx
├── mocha.opts
├── common.js
├── helpers.test.js
└── pagelet.test.js
├── .gitignore
├── error.html
├── pagelet.fragment
├── .travis.yml
├── helpers.js
├── LICENSE
├── package.json
├── README.md
└── index.js
/test/fixtures/style.css:
--------------------------------------------------------------------------------
1 | * { color: red }
2 |
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --reporter spec
2 | --ui bdd
3 |
--------------------------------------------------------------------------------
/test/fixtures/view.html:
--------------------------------------------------------------------------------
1 |
Some {test} fixture
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | .coveralls.yml
4 | coverage
--------------------------------------------------------------------------------
/error.html:
--------------------------------------------------------------------------------
1 | Error
2 |
3 | {reason}, {message}
4 |
--------------------------------------------------------------------------------
/test/fixtures/error.html:
--------------------------------------------------------------------------------
1 | Custom error template
2 |
3 | You failed!
--------------------------------------------------------------------------------
/test/fixtures/view.jsx:
--------------------------------------------------------------------------------
1 | /** @jsx React.DOM */
2 |
3 |
4 |
--------------------------------------------------------------------------------
/pagelet.fragment:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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;
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------