+32
| 'use strict'; | ||
| var path = require('path'); | ||
| /** | ||
| * Helper function to resolve assets on the pagelet. | ||
| * | ||
| * @param {Function} constructor The Pagelet constructor | ||
| * @param {String|Array} keys Name(s) of the property, e.g. [css, js]. | ||
| * @param {String} dir Optional absolute directory to resolve from. | ||
| * @returns {Pagelet} | ||
| * @api private | ||
| */ | ||
| exports.resolve = function resolve(constructor, keys, dir) { | ||
| var prototype = constructor.prototype; | ||
| keys = Array.isArray(keys) ? keys : [keys]; | ||
| keys.forEach(function each(key) { | ||
| if (!prototype[key]) return; | ||
| var stack = Array.isArray(prototype[key]) | ||
| ? prototype[key] | ||
| : [prototype[key]]; | ||
| prototype[key] = stack.filter(Boolean).map(function map(file) { | ||
| if (/^(http:|https:)?\/\//.test(file)) return file; | ||
| return path.resolve(dir || prototype.directory, file); | ||
| }); | ||
| }); | ||
| return constructor; | ||
| }; |
+22
| The MIT License (MIT) | ||
| Copyright (c) 2015 Arnout Kazemier, Martijn Swaagman, the Contributors. | ||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| SOFTWARE. | ||
| * { color: red } |
| describe('Helpers', function () { | ||
| 'use strict'; | ||
| var Pagelet = require('../').extend({ name: 'test' }) | ||
| , custom = '/unexisting/absolute/path/to/prepend' | ||
| , helpers = require('../helpers') | ||
| , assume = require('assume'); | ||
| describe('.resolve', function () { | ||
| var pagelet, P; | ||
| beforeEach(function () { | ||
| P = Pagelet.extend({ | ||
| directory: __dirname, | ||
| view: 'fixtures/view.html', | ||
| css: 'fixtures/style.css', | ||
| js: '//cdnjs.cloudflare.com/ajax/libs/d3/3.4.8/d3.min.js', | ||
| dependencies: [ | ||
| 'http://code.jquery.com/jquery-2.0.0.js', | ||
| 'fixtures/custom.js' | ||
| ] | ||
| }); | ||
| pagelet = new P; | ||
| }); | ||
| afterEach(function each() { | ||
| pagelet = null; | ||
| }); | ||
| it('is a function', function () { | ||
| assume(helpers.resolve).to.be.a('function'); | ||
| }); | ||
| it('will resolve provided property on prototype', function () { | ||
| var result = helpers.resolve(P, 'css'); | ||
| assume(result).to.equal(P); | ||
| assume(P.prototype.css).to.be.an('array'); | ||
| assume(P.prototype.css.length).to.equal(1); | ||
| assume(P.prototype.css[0]).to.equal(__dirname + '/fixtures/style.css'); | ||
| }); | ||
| it('can resolve multiple properties at once', function () { | ||
| helpers.resolve(P, ['css', 'js']); | ||
| assume(P.prototype.css).to.be.an('array'); | ||
| assume(P.prototype.js).to.be.an('array'); | ||
| assume(P.prototype.css.length).to.equal(1); | ||
| assume(P.prototype.js.length).to.equal(1); | ||
| }); | ||
| it('can be provided with a custom source directory', function () { | ||
| helpers.resolve(P, 'css', custom); | ||
| assume(P.prototype.css[0]).to.equal(custom + '/fixtures/style.css'); | ||
| }); | ||
| it('only resolves local files', function () { | ||
| helpers.resolve(P, 'js', custom); | ||
| assume(P.prototype.js[0]).to.not.include(custom); | ||
| assume(P.prototype.js[0]).to.equal('//cdnjs.cloudflare.com/ajax/libs/d3/3.4.8/d3.min.js'); | ||
| }); | ||
| it('can handle property values that are already an array', function () { | ||
| helpers.resolve(P, 'dependencies', custom); | ||
| assume(P.prototype.dependencies.length).to.equal(2); | ||
| assume(P.prototype.dependencies[0]).to.not.include(custom); | ||
| assume(P.prototype.dependencies[0]).to.equal('http://code.jquery.com/jquery-2.0.0.js'); | ||
| assume(P.prototype.dependencies[1]).to.equal(custom + '/fixtures/custom.js'); | ||
| }); | ||
| it('removes undefined values from the array before processing', function () { | ||
| var Undef = P.extend({ | ||
| dependencies: P.prototype.dependencies.concat( | ||
| undefined | ||
| ) | ||
| }); | ||
| assume(Undef.prototype.dependencies.length).to.equal(3); | ||
| helpers.resolve(Undef, 'dependencies', custom); | ||
| assume(Undef.prototype.dependencies.length).to.equal(2); | ||
| assume(Undef.prototype.dependencies).to.not.include(undefined); | ||
| }); | ||
| it('can be overriden', function () { | ||
| P.resolve = function () { | ||
| throw new Error('fucked'); | ||
| }; | ||
| P.on({}); | ||
| }); | ||
| }); | ||
| }); |
+20
-3
| language: node_js | ||
| node_js: | ||
| - "0.12" | ||
| - "0.11" | ||
| - "0.10" | ||
| - "0.11" | ||
| - "0.9" | ||
| - "iojs-v1.1" | ||
| - "iojs-v1.0" | ||
| before_install: | ||
| - "npm install -g npm@2.1.18" | ||
| script: | ||
| - "npm run test-travis" | ||
| after_script: | ||
| - "npm install coveralls@2.11.x && cat coverage/lcov.info | coveralls" | ||
| matrix: | ||
| fast_finish: true | ||
| allow_failures: | ||
| - node_js: "0.11" | ||
| - node_js: "0.9" | ||
| - node_js: "iojs-v1.1" | ||
| - node_js: "iojs-v1.0" | ||
| notifications: | ||
| irc: | ||
| channels: "irc.freenode.org#bigpipe" | ||
| channels: | ||
| - "irc.freenode.org#bigpipe" | ||
| on_success: change | ||
| on_failure: change |
+803
-345
| 'use strict'; | ||
| var jstringify = require('json-stringify-safe') | ||
| var Formidable = require('formidable').IncomingForm | ||
| , jstringify = require('json-stringify-safe') | ||
| , fabricate = require('fabricator') | ||
| , helpers = require('./helpers') | ||
| , debug = require('diagnostics') | ||
| , dot = require('dot-component') | ||
| , Stream = require('stream') | ||
| , Temper = require('temper') | ||
| , destroy = require('demolish') | ||
| , Route = require('routable') | ||
| , fuse = require('fusing') | ||
| , path = require('path'); | ||
| , async = require('async') | ||
| , path = require('path') | ||
| , url = require('url'); | ||
@@ -15,5 +19,9 @@ // | ||
| // | ||
| var slice = Array.prototype.slice | ||
| , temper; | ||
| var slice = Array.prototype.slice; | ||
| // | ||
| // Methods that needs data buffering. | ||
| // | ||
| var operations = 'POST, PUT, DELETE, PATCH'.toLowerCase().split(', '); | ||
| /** | ||
@@ -31,4 +39,4 @@ * Simple helper function to generate some what unique id's for given | ||
| /** | ||
| * A pagelet is the representation of an item, section, column, widget on the | ||
| * page. It's basically a small sand boxed application within your page. | ||
| * A pagelet is the representation of an item, section, column or widget. | ||
| * It's basically a small sandboxed application within your application. | ||
| * | ||
@@ -39,22 +47,44 @@ * @constructor | ||
| function Pagelet(options) { | ||
| if (!this) return new Pagelet(options); | ||
| this.fuse(); | ||
| options = options || {}; | ||
| this.writable('_active', null); // Are we active. | ||
| this.writable('substream', null); // Substream from Primus. | ||
| this.writable('temper', options.temper || temper); // Template parser. | ||
| // | ||
| // Use the temper instance on Pipe if available. | ||
| // | ||
| if (options.pipe && options.pipe._temper) options.temper = options.pipe._temper; | ||
| this.writable('id', options.id || [1, 1, 1, 1].map(generator).join('-')); | ||
| this._enabled = []; // Contains all enabled pagelets. | ||
| this._disabled = []; // Contains all disable pagelets. | ||
| this._active = null; // Are we active. | ||
| this._req = options.req; // Incoming HTTP request. | ||
| this._res = options.res; // Incoming HTTP response. | ||
| this._pipe = options.pipe; // Actual pipe instance. | ||
| this._params = options.params; // Params extracted from the route. | ||
| this._temper = options.temper; // Attach the Temper instance. | ||
| this._append = options.append || false; // Append content client-side. | ||
| this.bootstrap = options.bootstrap; // Reference to bootstrap Pagelet. | ||
| this.debug = debug('pagelet:'+ this.name); // Namespaced debug method | ||
| // | ||
| // Add an correctly namespaced debug method so it easier to see which pagelet | ||
| // is called by just checking the name of it. | ||
| // Allow overriding the reference to parent pagelet. | ||
| // A reference to the parent is normally set on the | ||
| // constructor prototype by optimize. | ||
| // | ||
| this.readable('debug', debug('pagelet:'+ this.name)); | ||
| if (options.parent) this._parent = options.parent; | ||
| } | ||
| fuse(Pagelet, Stream, { emits: false }); | ||
| fuse(Pagelet, require('eventemitter3')); | ||
| /** | ||
| * Unique id, useful for internal querying. | ||
| * | ||
| * @type {String} | ||
| * @public | ||
| */ | ||
| Pagelet.writable('id', null); | ||
| /** | ||
| * The name of this pagelet so it can checked to see if's enabled. In addition | ||
@@ -69,36 +99,33 @@ * to that, it can be injected in to placeholders using this name. | ||
| /** | ||
| * When enabled we will stream the submit of each form that is within a Pagelet | ||
| * to the server instead of using the default full page refreshes. After sending | ||
| * the data the resulting HTML will be used to only update the contents of the | ||
| * pagelet. | ||
| * The HTTP pathname that we should be matching against. | ||
| * | ||
| * If you want to opt-out of this with one form you can add | ||
| * a `data-pagelet-async="false"` attribute to the form element. | ||
| * @type {String|RegExp} | ||
| * @public | ||
| */ | ||
| Pagelet.writable('path', null); | ||
| /** | ||
| * Which HTTP methods should this pagelet accept. It can be a comma | ||
| * separated string or an array. | ||
| * | ||
| * @type {Boolean} | ||
| * @type {String|Array} | ||
| * @public | ||
| */ | ||
| Pagelet.writable('streaming', false); | ||
| Pagelet.writable('method', 'GET'); | ||
| /** | ||
| * These methods can be remotely called from the client. Please note that they | ||
| * are not set to the client, it will merely be executing on the server side. | ||
| * The default status code that we should send back to the user. | ||
| * | ||
| * ```js | ||
| * Pagelet.extend({ | ||
| * RPC: [ | ||
| * 'methodname', | ||
| * 'another' | ||
| * ], | ||
| * @type {Number} | ||
| * @public | ||
| */ | ||
| Pagelet.writable('statusCode', 200); | ||
| /** | ||
| * The pagelets that need to be loaded as children of this pagelet. | ||
| * | ||
| * methodname: function methodname(reply) { | ||
| * | ||
| * } | ||
| * }).on(module); | ||
| * ``` | ||
| * | ||
| * @type {Array} | ||
| * @type {Object} | ||
| * @public | ||
| */ | ||
| Pagelet.writable('RPC', []); | ||
| Pagelet.writable('pagelets', {}); | ||
@@ -112,6 +139,46 @@ /** | ||
| */ | ||
| Pagelet.writable('mode', 'html'); | ||
| Pagelet.writable('namespace', 'html'); | ||
| /** | ||
| * Conditionally load this pagelet. It can also be used authorization handler. | ||
| * With what kind of generation mode do we need to output the generated | ||
| * pagelets. We're supporting 3 different modes: | ||
| * | ||
| * - sync: Fully render without any fancy flushing of pagelets. | ||
| * - async: Render all pagelets async and flush them as fast as possible. | ||
| * - pipeline: Same as async but in the specified order. | ||
| * | ||
| * @type {String} | ||
| * @public | ||
| */ | ||
| Pagelet.writable('mode', 'async'); | ||
| /** | ||
| * Optional template engine preference. Useful when we detect the wrong template | ||
| * engine based on the view's file name. | ||
| * | ||
| * @type {String} | ||
| * @public | ||
| */ | ||
| Pagelet.writable('engine', ''); | ||
| /** | ||
| * Save the location where we got our resources from, this will help us with | ||
| * fetching assets from the correct location. | ||
| * | ||
| * @type {String} | ||
| * @public | ||
| */ | ||
| Pagelet.writable('directory', ''); | ||
| /** | ||
| * The environment that we're running this pagelet in. If this is set to | ||
| * `development` It would be verbose. | ||
| * | ||
| * @type {String} | ||
| * @public | ||
| */ | ||
| Pagelet.writable('env', (process.env.NODE_ENV || 'development').toLowerCase()); | ||
| /** | ||
| * Conditionally load this pagelet. It can also be used as authorization handler. | ||
| * If the incoming request is not authorized you can prevent this pagelet from | ||
@@ -121,3 +188,3 @@ * showing. The assigned function receives 3 arguments. | ||
| * - req, the http request that initialized the pagelet | ||
| * - list, array of pagelets that will be tried if this pagelet | ||
| * - list, array of pagelets that will be tried | ||
| * - done, a callback function that needs to be called with only a boolean. | ||
@@ -127,3 +194,3 @@ * | ||
| * Pagelet.extend({ | ||
| * if: function conditional(req, left, done) { | ||
| * if: function conditional(req, list, done) { | ||
| * done(true); // True indicates that the request is authorized for access. | ||
@@ -190,3 +257,3 @@ * } | ||
| */ | ||
| Pagelet.writable('view', ''); | ||
| Pagelet.writable('view', null); | ||
@@ -230,3 +297,3 @@ /** | ||
| /** | ||
| * The JavaScript files needed for this page. The location can be a string or | ||
| * The JavaScript files needed for this pagelet. The location can be a string or | ||
| * multiple paths in an array. This file needs to be included in order for | ||
@@ -268,2 +335,34 @@ * this pagelet to function. | ||
| /** | ||
| * Reference to parent Pagelet name. | ||
| * | ||
| * @type {Object} | ||
| * @private | ||
| */ | ||
| Pagelet.writable('_parent', null); | ||
| /** | ||
| * Set of optimized children Pagelet. | ||
| * | ||
| * @type {Object} | ||
| * @private | ||
| */ | ||
| Pagelet.writable('_children', {}); | ||
| /** | ||
| * Cataloged dependencies by extension. | ||
| * | ||
| * @type {Object} | ||
| * @private | ||
| */ | ||
| Pagelet.writable('_dependencies', {}); | ||
| /** | ||
| * Default content type of the Pagelet. | ||
| * | ||
| * @type {Object} | ||
| * @private | ||
| */ | ||
| Pagelet.writable('_contentType', 'text/html'); | ||
| /** | ||
| * Default asynchronous get function. Override to provide specific data to the | ||
@@ -280,2 +379,198 @@ * render function. | ||
| /** | ||
| * Get parameters that were extracted from the route. | ||
| * | ||
| * @type {Object} | ||
| * @public | ||
| */ | ||
| Pagelet.readable('params', { | ||
| enumerable: false, | ||
| get: function params() { | ||
| return this._params || this.bootstrap._params || Object.create(null); | ||
| } | ||
| }, true); | ||
| /** | ||
| * Report the length of the queue (e.g. amount of children). The length | ||
| * is increased with one as the reporting pagelet is part of the queue. | ||
| * | ||
| * @return {Number} Length of queue | ||
| * @api private | ||
| */ | ||
| Pagelet.set('length', function length() { | ||
| return this._children.length; | ||
| }); | ||
| /** | ||
| * Get and initialize a given child Pagelet. | ||
| * | ||
| * @param {String} name Name of the child pagelet. | ||
| * @returns {Array} The pagelet instances. | ||
| * @api public | ||
| */ | ||
| Pagelet.readable('child', function child(name) { | ||
| if (Array.isArray(name)) name = name[0]; | ||
| return (this.has(name) || this.has(name, true) || []).slice(0); | ||
| }); | ||
| /** | ||
| * Helper to invoke a specific route with an optionally provided method. | ||
| * Useful for serving a pagelet after handling POST requests for example. | ||
| * | ||
| * @param {String} route Registered path. | ||
| * @param {String} method Optional HTTP verb. | ||
| * @returns {Pagelet} fluent interface. | ||
| */ | ||
| Pagelet.readable('serve', function serve(route, method) { | ||
| var req = this._req | ||
| , res = this._res; | ||
| req.method = (method || 'get').toUpperCase(); | ||
| req.uri = url.parse(route); | ||
| this._pipe.router(req, res); | ||
| return this; | ||
| }); | ||
| /** | ||
| * Helper to check if the pagelet has a child pagelet by name, must use | ||
| * prototype.name since pagelets are not always constructed yet. | ||
| * | ||
| * @param {String} name Name of the pagelet. | ||
| * @param {String} enabled Make sure that we use the enabled array. | ||
| * @returns {Array} The constructors of matching Pagelets. | ||
| * @api public | ||
| */ | ||
| Pagelet.readable('has', function has(name, enabled) { | ||
| if (!name) return []; | ||
| if (enabled) return this._enabled.filter(function filter(pagelet) { | ||
| return pagelet.name === name; | ||
| }); | ||
| var pagelets = this._children | ||
| , i = pagelets.length | ||
| , pagelet; | ||
| while (i--) { | ||
| pagelet = pagelets[i][0]; | ||
| if ( | ||
| pagelet.prototype && pagelet.prototype.name === name | ||
| || pagelets.name === name | ||
| ) return pagelets[i]; | ||
| } | ||
| return []; | ||
| }); | ||
| /** | ||
| * Render execution flow. | ||
| * | ||
| * @api private | ||
| */ | ||
| Pagelet.readable('init', function init() { | ||
| var method = this._req.method.toLowerCase() | ||
| , pagelet = this; | ||
| // | ||
| // Only start reading the incoming POST request when we accept the incoming | ||
| // method for read operations. Render in a regular mode if we do not accept | ||
| // these requests. | ||
| // | ||
| if (~operations.indexOf(method)) { | ||
| var pagelets = this.child(this._req.query._pagelet) | ||
| , reader = this.read(pagelet); | ||
| this.debug('Processing %s request', method); | ||
| async.whilst(function work() { | ||
| return !!pagelets.length; | ||
| }, function process(next) { | ||
| var Child = pagelets.shift() | ||
| , child; | ||
| if (!(method in Pagelet.prototype)) return next(); | ||
| child = new Child({ pipe: pagelet._pipe }); | ||
| child.conditional(pagelet._req, pagelets, function allowed(accepted) { | ||
| if (!accepted) { | ||
| if (child.destroy) child.destroy(); | ||
| return next(); | ||
| } | ||
| reader.before(child[method], child); | ||
| }); | ||
| }, function nothing() { | ||
| if (method in pagelet) { | ||
| reader.before(pagelet[method], pagelet); | ||
| } else { | ||
| pagelet[pagelet.mode](); | ||
| } | ||
| }); | ||
| } else { | ||
| this[this.mode](); | ||
| } | ||
| }); | ||
| /** | ||
| * Start buffering and reading the incoming request. | ||
| * | ||
| * @returns {Form} | ||
| * @api private | ||
| */ | ||
| Pagelet.readable('read', function read() { | ||
| var form = new Formidable | ||
| , pagelet = this | ||
| , fields = {} | ||
| , files = {} | ||
| , context | ||
| , before; | ||
| form.on('progress', function progress(received, expected) { | ||
| // | ||
| // @TODO if we're not sure yet if we should handle this form, we should only | ||
| // buffer it to a predefined amount of bytes. Once that limit is reached we | ||
| // need to `form.pause()` so the client stops uploading data. Once we're | ||
| // given the heads up, we can safely resume the form and it's uploading. | ||
| // | ||
| }).on('field', function field(key, value) { | ||
| fields[key] = value; | ||
| }).on('file', function file(key, value) { | ||
| files[key] = value; | ||
| }).on('error', function error(err) { | ||
| pagelet.capture(err, true); | ||
| fields = files = {}; | ||
| }).on('end', function end() { | ||
| form.removeAllListeners(); | ||
| if (before) { | ||
| before.call(context, fields, files); | ||
| } | ||
| }); | ||
| /** | ||
| * Add a hook for adding a completion callback. | ||
| * | ||
| * @param {Function} callback | ||
| * @returns {Form} | ||
| * @api public | ||
| */ | ||
| form.before = function befores(callback, contexts) { | ||
| if (form.listeners('end').length) { | ||
| form.resume(); // Resume a possible buffered post. | ||
| before = callback; | ||
| context = contexts; | ||
| return form; | ||
| } | ||
| callback.call(contexts || context, fields, files); | ||
| return form; | ||
| }; | ||
| return form.parse(this._req); | ||
| }); | ||
| /** | ||
| * A safe and fast(er) alternative to the `json-stringify-save` as uses the | ||
@@ -313,2 +608,252 @@ * replacer to make the transformation save. This is really costly for larger | ||
| /** | ||
| * Discover pagelets that we're allowed to use. | ||
| * | ||
| * @returns {Pagelet} fluent interface | ||
| * @api private | ||
| */ | ||
| Pagelet.readable('discover', function discover() { | ||
| if (!this.length) return this.emit('discover'); | ||
| var req = this._req | ||
| , res = this._res | ||
| , pagelet = this; | ||
| // | ||
| // We need to do an async map/filter of the pagelets, in order to this as | ||
| // efficient as possible we're going to use a reduce. | ||
| // | ||
| async.reduce(this._children, { | ||
| disabled: [], | ||
| enabled: [] | ||
| }, function reduce(memo, children, next) { | ||
| children = children.slice(0); | ||
| var child, last; | ||
| async.whilst(function work() { | ||
| return children.length && !child; | ||
| }, function work(next) { | ||
| var Child = children.shift() | ||
| , test = new Child({ | ||
| bootstrap: pagelet.bootstrap, | ||
| pipe: pagelet._pipe, | ||
| res: res, | ||
| req: req | ||
| }); | ||
| test.conditional(req, children, function conditionally(accepted) { | ||
| if (last && last.destroy) last.destroy(); | ||
| if (accepted) child = test; | ||
| else last = test; | ||
| next(!!child); | ||
| }); | ||
| }, function found() { | ||
| if (child) memo.enabled.push(child); | ||
| else memo.disabled.push(last); | ||
| next(undefined, memo); | ||
| }); | ||
| }, function discovered(err, children) { | ||
| pagelet._disabled = children.disabled; | ||
| pagelet._enabled = children.enabled.concat(pagelet); | ||
| pagelet._enabled.forEach(function initialize(child) { | ||
| if ('function' === typeof child.initialize) child.initialize(); | ||
| }); | ||
| pagelet.debug('Initialized all allowed pagelets'); | ||
| pagelet.emit('discover'); | ||
| }); | ||
| return this; | ||
| }); | ||
| /** | ||
| * Mode: Synchronous | ||
| * Output the pagelets fully rendered in the HTML template. | ||
| * | ||
| * @TODO remove pagelet's that have `authorized` set to `false` | ||
| * @TODO Also write the CSS and JavaScript. | ||
| * | ||
| * @api private | ||
| */ | ||
| Pagelet.readable('sync', function synchronous() { | ||
| var pagelet = this; | ||
| // | ||
| // Because we're synchronously rendering the pagelets we need to discover | ||
| // which one's are enabled before we send the bootstrap code so it can include | ||
| // the CSS files of the enabled pagelets in the HEAD of the page so there is | ||
| // styling available. | ||
| // | ||
| pagelet.once('discover', function discovered() { | ||
| pagelet.debug('Processing the pagelets in `sync` mode'); | ||
| async.each(pagelet._enabled.concat(pagelet._disabled), function render(child, next) { | ||
| pagelet.debug('Invoking pagelet %s/%s render', child.name, child.id); | ||
| child.render({ mode: 'sync' }, function rendered(error, content) { | ||
| if (error) return render(child.capture(error), next); | ||
| child.write(content); | ||
| next(); | ||
| }); | ||
| }, function done() { | ||
| pagelet.bootstrap.render().reduce().end(); | ||
| }); | ||
| }).discover(); | ||
| }); | ||
| /** | ||
| * Mode: Asynchronous | ||
| * Output the pagelets as fast as possible. | ||
| * | ||
| * @api private | ||
| */ | ||
| Pagelet.readable('async', function asynchronous() { | ||
| var pagelet = this; | ||
| // | ||
| // Flush the initial headers asap so the browser can start detect encoding | ||
| // start downloading assets and prepare for rendering additional pagelets. | ||
| // | ||
| pagelet.bootstrap.render().flush(function headers(error) { | ||
| if (error) return pagelet.capture(error, true); | ||
| pagelet.once('discover', function discovered() { | ||
| pagelet.debug('Processing the pagelets in `async` mode'); | ||
| async.each(pagelet._enabled.concat(pagelet._disabled), function render(child, next) { | ||
| pagelet.debug('Invoking pagelet %s/%s render', child.name, child.id); | ||
| child.render({ | ||
| data: pagelet._pipe._compiler.pagelet(child) | ||
| }, function rendered(error, content) { | ||
| if (error) return render(child.capture(error), next); | ||
| child.write(content).flush(next); | ||
| }); | ||
| }, pagelet.end.bind(pagelet)); | ||
| }).discover(); | ||
| }); | ||
| }); | ||
| /** | ||
| * Mode: pipeline | ||
| * Output the pagelets as fast as possible but in order. | ||
| * | ||
| * @returns {Pagelet} fluent interface. | ||
| * @api private | ||
| */ | ||
| Pagelet.readable('pipeline', function render() { | ||
| throw new Error('Not Implemented'); | ||
| }); | ||
| /** | ||
| * Process the pagelet for an async or pipeline based render flow. | ||
| * | ||
| * @param {String} name Optional name, defaults to pagelet.name. | ||
| * @param {Mixed} chunk Content of Pagelet. | ||
| * @returns {Bootstrap} Reference to bootstrap Pagelet. | ||
| * @api private | ||
| */ | ||
| Pagelet.readable('write', function write(name, chunk) { | ||
| if (!chunk) { | ||
| chunk = name; | ||
| name = this.name; | ||
| } | ||
| // | ||
| // The chunk could potentially be an error, capture it before | ||
| // its pushed to the queue. | ||
| // | ||
| if (chunk instanceof Error) return this.capture(chunk); | ||
| this.debug('Queueing data chunk'); | ||
| return this.bootstrap.queue(name, this._parent, chunk); | ||
| }); | ||
| /** | ||
| * Close the connection once all pagelets are sent. | ||
| * | ||
| * @param {Mixed} chunk Fragment of data. | ||
| * @returns {Boolean} Closed the connection. | ||
| * @api private | ||
| */ | ||
| Pagelet.readable('end', function end(chunk) { | ||
| var pagelet = this; | ||
| // | ||
| // Write data chunk to the queue. | ||
| // | ||
| if (chunk) this.write(chunk); | ||
| // | ||
| // Do not close the connection before all pagelets are send. | ||
| // | ||
| if (this.bootstrap.length > 0) { | ||
| this.debug('Not all pagelets have been written, (%s out of %s)', | ||
| this.bootstrap.length, this.length | ||
| ); | ||
| return false; | ||
| } | ||
| // | ||
| // Everything is processed, close the connection and clean up references. | ||
| // | ||
| this.bootstrap.flush(function close(error) { | ||
| if (error) return pagelet.capture(error, true); | ||
| pagelet.debug('Closed the connection'); | ||
| pagelet._res.end(); | ||
| }); | ||
| return true; | ||
| }); | ||
| /** | ||
| * We've received an error. Close down pagelet and display a 500 | ||
| * error Pagelet instead. | ||
| * | ||
| * @TODO handle the case when we've already flushed the initial bootstrap code | ||
| * to the client and we're presented with an error. | ||
| * | ||
| * @param {Error} error Optional error argument to trigger the error pagelet. | ||
| * @param {Boolean} bootstrap Trigger full bootstrap if true. | ||
| * @returns {Pagelet} Reference to Pagelet. | ||
| * @api private | ||
| */ | ||
| Pagelet.readable('capture', function capture(error, bootstrap) { | ||
| this.debug('Captured an error: %s, displaying error pagelet instead', error); | ||
| return this._pipe.status(this, 500, error, bootstrap); | ||
| }); | ||
| /** | ||
| * The Content-Type of the response. This defaults to text/html with a charset | ||
| * preset inherited from the charset property. | ||
| * | ||
| * @type {String} | ||
| * @public | ||
| */ | ||
| Pagelet.set('contentType', function get() { | ||
| return this._contentType +';charset='+ this.charset; | ||
| }, function set(value) { | ||
| return this._contentType = value; | ||
| }); | ||
| /** | ||
| * Returns reference to bootstrap Pagelet, which could be the Pagelet itself. | ||
| * Allows more chaining and valid bootstrap Pagelet references. | ||
| * | ||
| * @type {String} | ||
| * @public | ||
| */ | ||
| Pagelet.set('bootstrap', function get() { | ||
| return !this._bootstrap && this.name === 'bootstrap' ? this : this._bootstrap || {}; | ||
| }, function set(value) { | ||
| return this._bootstrap = value; | ||
| }); | ||
| /** | ||
| * Checks if we're an active Pagelet or if we still need to a do an check | ||
@@ -350,4 +895,6 @@ * against the `if` function. | ||
| var context = options.context || this | ||
| , compiler = this._pipe._compiler | ||
| , mode = options.mode || 'async' | ||
| , data = options.data || {} | ||
| , temper = this.temper | ||
| , temper = this._temper | ||
| , query = this.query | ||
@@ -367,18 +914,14 @@ , pagelet = this; | ||
| if (!active) content = ''; | ||
| if (mode === 'sync') return fn.call(context, undefined, content); | ||
| if (options.substream || pagelet.page && pagelet.page.mode === 'sync') { | ||
| data.view = content; | ||
| return fn.call(context, undefined, data); | ||
| } | ||
| data.id = data.id || pagelet.id; // Pagelet id. | ||
| data.path = data.path || pagelet.path; // Reference to the path. | ||
| data.mode = data.mode || pagelet.mode; // Pagelet render mode. | ||
| data.remove = active ? false : pagelet.remove; // Remove from DOM. | ||
| data.parent = pagelet._parent; // Send parent name along. | ||
| data.append = pagelet._append; // Content should be appended. | ||
| data.remaining = pagelet.bootstrap.length; // Remaining pagelets number. | ||
| data.id = data.id || pagelet.id; // Pagelet id. | ||
| data.mode = data.mode || pagelet.mode; // Pagelet render mode. | ||
| data.rpc = data.rpc || pagelet.RPC; // RPC methods. | ||
| data.remove = active ? false : pagelet.remove; // Remove from DOM. | ||
| data.streaming = !!pagelet.streaming; // Submit streaming. | ||
| data.parent = pagelet._parent; // Send parent name along. | ||
| data.hash = { | ||
| error: temper.fetch(pagelet.error).hash.client, // MD5 hash of error view. | ||
| client: temper.fetch(pagelet.view).hash.client // MD5 hash of client view. | ||
| }; | ||
| data.error = compiler.resolve(pagelet.error); // Path of error view. | ||
| data.client = compiler.resolve(pagelet.view); // Path of client view. | ||
@@ -397,2 +940,3 @@ data = pagelet.stringify(data, function sanitize(key, data) { | ||
| fn.call(context, undefined, pagelet.fragment | ||
| .replace(/\{pagelet:id\}/g, pagelet.id) | ||
| .replace(/\{pagelet:name\}/g, pagelet.name) | ||
@@ -406,3 +950,3 @@ .replace(/\{pagelet:template\}/g, content.replace(/<!--(.|\s)*?-->/, '')) | ||
| return this.conditional(this.page.req, options.pagelets, function auth(enabled) { | ||
| return this.conditional(this._req, options.pagelets, function auth(enabled) { | ||
| if (!enabled) return fragment(''); | ||
@@ -419,2 +963,8 @@ | ||
| // | ||
| // Add some template defaults. | ||
| // | ||
| result = result || {}; | ||
| if (!('path' in result)) result.path = pagelet.path; | ||
| // | ||
| // We've made it this far, but now we have to cross our fingers and HOPE | ||
@@ -429,7 +979,7 @@ // that our given template can actually handle the data correctly | ||
| if (err) { | ||
| pagelet.debug('render %s/%s resulted in a error', pagelet.name, pagelet.id, err); | ||
| pagelet.debug('Render %s/%s resulted in a error', pagelet.name, pagelet.id, err); | ||
| throw err; // Throw so we can capture it again. | ||
| } | ||
| content = view(result || {}); | ||
| content = view(result); | ||
| } catch (e) { | ||
@@ -457,3 +1007,3 @@ // | ||
| // | ||
| if ('object' === typeof result && Array.isArray(query)) { | ||
| if ('object' === typeof result && Array.isArray(query) && query.length) { | ||
| data.data = query.reduce(function find(memo, q) { | ||
@@ -471,119 +1021,2 @@ memo[q] = dot.get(result, q); | ||
| /** | ||
| * Connect with a Primus substream. | ||
| * | ||
| * @param {Spark} spark The Primus connection. | ||
| * @param {Function} next The completion callback | ||
| * @returns {Pagelet} | ||
| * @api private | ||
| */ | ||
| Pagelet.readable('connect', function connect(spark, next) { | ||
| var pagelet = this; | ||
| /** | ||
| * Create a new Substream. | ||
| * | ||
| * @param {Boolean} enabled Allowed to use this pagelet. | ||
| * @returns {Pagelet} | ||
| * @api private | ||
| */ | ||
| return this.conditional(spark.request, [], function substream(enabled) { | ||
| if (!enabled) return next(new Error('Unauthorized to access this pagelet')); | ||
| var stream = pagelet.substream = spark.substream(pagelet.name) | ||
| , log = debug('pagelet:primus:'+ pagelet.name); | ||
| log('created a new substream'); | ||
| stream.once('end', pagelet.emits('end', function (arg) { | ||
| log('closing substream'); | ||
| return arg; | ||
| })); | ||
| stream.on('data', function streamed(data) { | ||
| log('incoming packet %s', data.type); | ||
| switch (data.type) { | ||
| case 'rpc': | ||
| pagelet.call(data); | ||
| break; | ||
| case 'emit': | ||
| Stream.prototype.emit.apply(pagelet, [data.name].concat(data.args)); | ||
| break; | ||
| case 'get': | ||
| pagelet.render({ substream: true }, function renderd(err, fragment) { | ||
| stream.write({ type: 'fragment', frag: fragment, err: err }); | ||
| }); | ||
| break; | ||
| case 'post': | ||
| case 'put': | ||
| if (!(data.type in pagelet)) { | ||
| return stream.write({ type: data.type, err: new Error('Method not supported by pagelet') }); | ||
| } | ||
| pagelet[data.type](data.body || {}, data.files || [], function processed(err, context) { | ||
| if (err) return stream.write({ type: 'err', err: err }); | ||
| pagelet.render({ data: context, substream: true }, function rendered(err, fragment) { | ||
| if (err) return stream.write({ type: 'err', err: err }); | ||
| stream.write({ type: 'fragment', frag: fragment, err: err }); | ||
| }); | ||
| }); | ||
| break; | ||
| default: | ||
| log('unknown packet type %s, ignoring packet', data.type); | ||
| break; | ||
| } | ||
| }); | ||
| next(undefined, pagelet); | ||
| return pagelet; | ||
| }); | ||
| }); | ||
| /** | ||
| * Simple emit wrapper that returns a function that emits an event once it's | ||
| * called | ||
| * | ||
| * ```js | ||
| * example.on('close', example.emits('close')); | ||
| * ``` | ||
| * | ||
| * @param {String} event Name of the event that we should emit. | ||
| * @param {Function} parser The last argument, if it's a function is a arg parser | ||
| * @api public | ||
| */ | ||
| Pagelet.prototype.emits = function emits() { | ||
| var args = slice.call(arguments, 0) | ||
| , self = this | ||
| , parser; | ||
| // | ||
| // Assume that if the last given argument is a function, it would be | ||
| // a parser. | ||
| // | ||
| if ('function' === typeof args[args.length - 1]) { | ||
| parser = args.pop(); | ||
| } | ||
| return function emit(arg) { | ||
| if (!self.listeners(args[0]).length) return false; | ||
| if (parser) { | ||
| arg = parser.apply(self, arguments); | ||
| if (!Array.isArray(arg)) arg = [arg]; | ||
| } else { | ||
| arg = slice.call(arguments, 0); | ||
| } | ||
| return Stream.prototype.emit.apply(self, args.concat(arg)); | ||
| }; | ||
| }; | ||
| /** | ||
| * Authenticate the Pagelet. | ||
@@ -600,2 +1033,7 @@ * | ||
| if ('function' !== typeof fn) { | ||
| fn = list; | ||
| list = []; | ||
| } | ||
| /** | ||
@@ -624,34 +1062,2 @@ * Callback for the `pagelet.if` function to see if we're enabled or disabled. | ||
| /** | ||
| * Call an RPC method. | ||
| * | ||
| * @param {Object} data The RPC call information. | ||
| * @api private | ||
| */ | ||
| Pagelet.readable('call', function calls(data) { | ||
| var index = this.RPC.indexOf(data.method) | ||
| , fn = this[data.method] | ||
| , pagelet = this | ||
| , err; | ||
| if (!~index || 'function' !== typeof fn) return this.substream.write({ | ||
| args: [new Error('RPC method is not known')], | ||
| type: 'rpc', | ||
| id: data.id | ||
| }); | ||
| // | ||
| // Our RPC pattern is a callback first pattern, where the callback is the | ||
| // first argument that a function receives. This makes it a lot easier to add | ||
| // a variable length of arguments to a function call. | ||
| // | ||
| fn.apply(pagelet, [function reply() { | ||
| pagelet.substream.write({ | ||
| args: slice.call(arguments, 0), | ||
| type: 'rpc', | ||
| id: data.id | ||
| }); | ||
| }].concat(data.args)); | ||
| }); | ||
| /** | ||
| * Destroy the pagelet and remove all the back references so it can be safely | ||
@@ -662,40 +1068,10 @@ * garbage collected. | ||
| */ | ||
| Pagelet.readable('destroy', function destroy() { | ||
| if (this.substream) this.substream.end(); | ||
| Pagelet.readable('destroy', destroy([ | ||
| '_temper', '_pipe', '_enabled', '_disabled', '_pagelets' | ||
| ], { | ||
| after: 'removeAllListeners' | ||
| })); | ||
| this.temper = null; | ||
| this.removeAllListeners(); | ||
| return this; | ||
| }); | ||
| /** | ||
| * Helper function to resolve assets on the pagelet. | ||
| * | ||
| * @param {String|Array} keys Name(s) of the property, e.g. [css, js]. | ||
| * @param {String} dir Optional absolute directory to resolve from. | ||
| * @returns {Pagelet} | ||
| * @api public | ||
| */ | ||
| Pagelet.resolve = function resolve(keys, dir) { | ||
| var prototype = this.prototype; | ||
| keys = Array.isArray(keys) ? keys : [keys]; | ||
| keys.forEach(function each(key) { | ||
| if (!prototype[key]) return; | ||
| var stack = Array.isArray(prototype[key]) | ||
| ? prototype[key] | ||
| : [prototype[key]]; | ||
| prototype[key] = stack.filter(Boolean).map(function map(file) { | ||
| if (/^(http:|https:)?\/\//.test(file)) return file; | ||
| return path.resolve(dir || prototype.directory, file); | ||
| }); | ||
| }); | ||
| return this; | ||
| }; | ||
| /** | ||
| * Expose the Pagelet on the exports and parse our the directory. This ensures | ||
@@ -721,14 +1097,7 @@ * that we can properly resolve all relative assets: | ||
| prototype.error = prototype.error | ||
| ? path.resolve(dir, prototype.error) | ||
| : path.resolve(__dirname, 'error.html'); | ||
| // | ||
| // Map all dependencies to an absolute path or URL. | ||
| // Resolve the view and error templates to ensure | ||
| // absolute paths are provided to Temper. | ||
| // | ||
| Pagelet.resolve.call(this, ['css', 'js', 'dependencies']); | ||
| // | ||
| // Resolve the view to make sure an absolute path is provided to Temper. | ||
| // | ||
| if (prototype.error) prototype.error = path.resolve(dir, prototype.error); | ||
| if (prototype.view) prototype.view = path.resolve(dir, prototype.view); | ||
@@ -740,124 +1109,213 @@ | ||
| /** | ||
| * Optimize the prototypes of the Pagelet to reduce work when we're actually | ||
| * serving the requests. | ||
| * Discover all pagelets recursive. Fabricate will create constructable | ||
| * instances from the provided value of prototype.pagelets. | ||
| * | ||
| * @param {String} parent Reference to the parent pagelet name. | ||
| * @return {Array} collection of pagelets instances. | ||
| * @api public | ||
| */ | ||
| Pagelet.children = function children(parent, stack) { | ||
| var pagelets = this.prototype.pagelets | ||
| , log = debug('pagelet:'+ parent); | ||
| stack = stack || []; | ||
| if (!pagelets || !Object.keys(pagelets).length) return stack; | ||
| return fabricate(pagelets, { | ||
| source: this.prototype.directory, | ||
| recursive: 'string' === typeof pagelets | ||
| }).reduce(function each(stack, Pagelet) { | ||
| // | ||
| // Pagelet could be conditional, simple crawl this function | ||
| // again to get the children of each conditional. | ||
| // | ||
| if (Array.isArray(Pagelet)) return Pagelet.reduce(each, []); | ||
| var name = Pagelet.prototype.name; | ||
| log('Recursive discovery of child pagelet %s', name); | ||
| // | ||
| // We need to extend the pagelet if it already has a _parent name reference | ||
| // or will accidentally override it. This can happen when you extend a parent | ||
| // pagelet with children and alter the parent's name. The extended parent and | ||
| // regular parent still point to the same child pagelets. So when we try to | ||
| // set the proper parent, these pagelets will override the _parent property | ||
| // unless we create a new fresh instance and set it on that instead. | ||
| // | ||
| if (Pagelet.prototype._parent && name !== parent) { | ||
| Pagelet = Pagelet.extend(); | ||
| } | ||
| Pagelet.prototype._parent = parent; | ||
| return Pagelet.children(name, stack.concat(Pagelet)); | ||
| }, stack); | ||
| }; | ||
| /** | ||
| * Optimize the prototypes of Pagelets to reduce work when we're actually | ||
| * serving the requests via BigPipe. | ||
| * | ||
| * Options: | ||
| * | ||
| * - temper: A custom temper instance we want to use to compile the templates. | ||
| * - transform: Transformation callback so plugins can hook in the optimizer. | ||
| * | ||
| * @param {Object} options Optimization configuration. | ||
| * @param {Function} next Completion callback for async execution. | ||
| * @returns {Pagelet} | ||
| * @api private | ||
| * @api public | ||
| */ | ||
| Pagelet.optimize = function optimize(options, next) { | ||
| var prototype = this.prototype | ||
| , name = prototype.name | ||
| , async = false | ||
| , err; | ||
| Pagelet.optimize = function optimize(options, done) { | ||
| if ('function' === typeof options) { | ||
| done = options; | ||
| options = {}; | ||
| } | ||
| options = options || {}; | ||
| options.temper = options.temper || temper || (temper = new Temper()) ; | ||
| var stack = [] | ||
| , Pagelet = this | ||
| , pipe = options.pipe || {} | ||
| , transform = options.transform || {} | ||
| , temper = options.temper || pipe._temper | ||
| , before, after; | ||
| // | ||
| // Prefetch the template if a view is available. Resolve the view | ||
| // to make sure an absolute path is provided to Temper. | ||
| // Check if before listener is found. Add before emit to the stack. | ||
| // This async function will be called before optimize. | ||
| // | ||
| if (prototype.view) { | ||
| prototype.view = path.resolve(prototype.directory, prototype.view); | ||
| options.temper.prefetch(prototype.view, prototype.engine); | ||
| } | ||
| if (pipe._events && 'transform:pagelet:before' in pipe._events) { | ||
| before = pipe._events['transform:pagelet:before'].length || 1; | ||
| // | ||
| // Ensure we have a custom error page when we fail to render this fragment. | ||
| // | ||
| if (prototype.error) { | ||
| options.temper.prefetch(prototype.error, path.extname(prototype.error).slice(1)); | ||
| stack.push(function run(next) { | ||
| var n = 0; | ||
| transform.before(Pagelet, function ran(error, Pagelet) { | ||
| if (error || ++n === before) return next(error, Pagelet); | ||
| }); | ||
| }); | ||
| } | ||
| // | ||
| // Map all dependencies to an absolute path or URL. | ||
| // If transform.before was not pushed on the stack, optimizer needs | ||
| // to called with a reference to Pagelet. | ||
| // | ||
| Pagelet.resolve.call(this, ['css', 'js', 'dependencies']); | ||
| stack.push(!stack.length ? async.apply(optimizer, Pagelet) : optimizer); | ||
| // | ||
| // Support lowercase variant of RPC | ||
| // Check if after listener is found. Add after emit to the stack. | ||
| // This async function will be called after optimize. | ||
| // | ||
| if ('rpc' in prototype) { | ||
| prototype.RPC = prototype.rpc; | ||
| delete prototype.rpc; | ||
| } | ||
| if (pipe._events && 'transform:pagelet:after' in pipe._events) { | ||
| after = pipe._events['transform:pagelet:after'].length || 1; | ||
| if ('string' === typeof prototype.RPC) { | ||
| prototype.RPC = prototype.RPC.split(/[\s|\,]+/); | ||
| stack.push(function run(Pagelet, next) { | ||
| var n = 0; | ||
| transform.after(Pagelet, function ran(error, Pagelet) { | ||
| if (error || ++n === after) return next(error, Pagelet); | ||
| }); | ||
| }); | ||
| } | ||
| // | ||
| // Validate the existance of the RPC methods, this reduces possible typo's | ||
| // Run the stack in series. This ensures that before hooks are run | ||
| // prior to optimizing and after hooks are ran post optimizing. | ||
| // | ||
| prototype.RPC.forEach(function validate(method) { | ||
| if (!(method in prototype)) return err = new Error( | ||
| name +' is missing RPC function `'+ method +'` on prototype' | ||
| ); | ||
| async.waterfall(stack, done); | ||
| if ('function' !== typeof prototype[method]) return err = new Error( | ||
| name +'#'+ method +' is not function which is required for RPC usage' | ||
| ); | ||
| }); | ||
| /** | ||
| * Optimize the pagelet. This function is called by default as part of | ||
| * the async stack. | ||
| * | ||
| * @param {Function} next Completion callback | ||
| * @api private | ||
| */ | ||
| function optimizer(Pagelet, next) { | ||
| var prototype = Pagelet.prototype | ||
| , method = prototype.method | ||
| , router = prototype.path | ||
| , name = prototype.name | ||
| , log = debug('pagelet:'+ name); | ||
| // | ||
| // Allow plugins to hook in the transformation process, so emit it when | ||
| // all our transformations are done and before we create a copy of the | ||
| // "fixed" properties which later can be re-used again to restore | ||
| // a generated instance to it's original state. | ||
| // | ||
| if ('function' === typeof options.transform && !err) { | ||
| if (options.transform.length === 2) async = true; | ||
| options.transform(this, next); | ||
| } | ||
| // | ||
| // Generate a unique ID used for real time connection lookups. | ||
| // | ||
| prototype.id = options.id || [1, 1, 1, 1].map(generator).join('-'); | ||
| if (!async) process.nextTick(next.bind(next, err)); | ||
| // | ||
| // Parse the methods to an array of accepted HTTP methods. We'll only accept | ||
| // these requests and should deny every other possible method. | ||
| // | ||
| log('Optimizing pagelet'); | ||
| if (!Array.isArray(method)) method = method.split(/[\s\,]+?/); | ||
| Pagelet.method = method.filter(Boolean).map(function transformation(method) { | ||
| return method.toUpperCase(); | ||
| }); | ||
| return this; | ||
| }; | ||
| // | ||
| // Add the actual HTTP route and available HTTP methods. | ||
| // | ||
| if (router) { | ||
| log('Instantiating router for path %s', router); | ||
| Pagelet.router = new Route(router); | ||
| } | ||
| /** | ||
| * Discover all pagelets recursive. Fabricate will create constructable instances | ||
| * from the provided value of prototype.pagelets. | ||
| * | ||
| * @param {Pagelet} parent Reference to the parent pagelet. | ||
| * @return {Array} collection of pagelets instances. | ||
| * @api public | ||
| */ | ||
| Pagelet.traverse = function traverse(parent) { | ||
| var pagelets = this.prototype.pagelets | ||
| , log = debug('bigpipe:pagelet') | ||
| , found = [this]; | ||
| // | ||
| // Prefetch the template if a view is available. The view property is | ||
| // mandatory but it's quite silly to enforce this if the pagelet is | ||
| // just doing a redirect. We can check for this edge case by | ||
| // checking if the set statusCode is in the 300~ range. | ||
| // | ||
| if (prototype.view) { | ||
| prototype.view = path.resolve(prototype.directory, prototype.view); | ||
| temper.prefetch(prototype.view, prototype.engine); | ||
| } else if (!(prototype.statusCode >= 300 && prototype.statusCode < 400)) { | ||
| return next(new Error( | ||
| 'The '+ name +' pagelet for path '+ router +' should have a .view property.' | ||
| )); | ||
| } | ||
| if (!pagelets) return found; | ||
| // | ||
| // Ensure we have a custom error pagelet when we fail to render this fragment. | ||
| // | ||
| if (prototype.error) { | ||
| temper.prefetch(prototype.error, path.extname(prototype.error).slice(1)); | ||
| } | ||
| pagelets = fabricate(pagelets, { recursive: false }); | ||
| pagelets.forEach(function each(Pagelet) { | ||
| log('Recursive discovery of child pagelets from %s', parent); | ||
| // | ||
| // Map all dependencies to an absolute path or URL. | ||
| // | ||
| helpers.resolve(Pagelet, ['css', 'js', 'dependencies']); | ||
| // | ||
| // We need to extend the pagelet if it already has a _parent name reference | ||
| // or will accidentally override it. This you have Pagelet with child | ||
| // pagelet. And you extend the parent pagelet so it receives a new name. But | ||
| // the extended parent and regular parent still point to the same child | ||
| // pagelet. So when we try to traverse these pagelets we will override | ||
| // _parent property unless we create a new fresh instance and set it on that | ||
| // instead. | ||
| // Find all child pagelets and optimize the found children. | ||
| // | ||
| if (Pagelet.prototype._parent && Pagelet.prototype.name !== parent) { | ||
| Pagelet = Pagelet.extend(); | ||
| } | ||
| async.map(Pagelet.children(name), function map(Child, step) { | ||
| if (Array.isArray(Child)) return async.map(Child, map, step); | ||
| Pagelet.prototype._parent = parent; | ||
| Child.optimize({ | ||
| temper: temper, | ||
| pipe: pipe, | ||
| transform: { | ||
| before: pipe.emits && pipe.emits('transform:pagelet:before'), | ||
| after: pipe.emits && pipe.emits('transform:pagelet:after') | ||
| } | ||
| }, step); | ||
| }, function optimized(error, children) { | ||
| log('optimized all %d child pagelets', children.length); | ||
| Array.prototype.push.apply(found, Pagelet.traverse(Pagelet.prototype.name)); | ||
| }); | ||
| if (error) return next(error); | ||
| return found; | ||
| // | ||
| // Store the optimized children on the prototype, wrapping the Pagelet | ||
| // in an array makes it a lot easier to work with conditional Pagelets. | ||
| // | ||
| prototype._children = children.map(function map(Pagelet) { | ||
| return Array.isArray(Pagelet) ? Pagelet : [Pagelet]; | ||
| }); | ||
| // | ||
| // Always return a reference to the parent Pagelet. | ||
| // Otherwise the stack of parents would be infested | ||
| // with children returned by this async.map. | ||
| // | ||
| next(null, Pagelet); | ||
| }); | ||
| } | ||
| }; | ||
@@ -864,0 +1322,0 @@ |
+17
-13
| { | ||
| "name": "pagelet", | ||
| "version": "0.8.2", | ||
| "version": "1.0.0-alpha", | ||
| "description": "pagelet", | ||
| "main": "index.js", | ||
| "scripts": { | ||
| "test": "illuminati", | ||
| "coverage": "NODE_ENV=test ./node_modules/.bin/istanbul cover ./node_modules/.bin/_mocha $(find test -name '*.test.js') --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js || true && rm -rf ./coverage" | ||
| "100%": "istanbul check-coverage --statements 100 --functions 100 --lines 100 --branches 100", | ||
| "test": "mocha $(find test -name '*.test.js')", | ||
| "watch": "mocha --watch $(find test -name '*.test.js')", | ||
| "coverage": "istanbul cover ./node_modules/.bin/_mocha -- $(find test -name '*.test.js')", | ||
| "test-travis": "istanbul cover node_modules/.bin/_mocha --report lcovonly -- $(find test -name '*.test.js')" | ||
| }, | ||
@@ -25,20 +28,21 @@ "repository": { | ||
| "dependencies": { | ||
| "async": "0.9.x", | ||
| "demolish": "1.0.x", | ||
| "diagnostics": "0.0.x", | ||
| "dot-component": "0.1.x", | ||
| "eventemitter3": "0.1.x", | ||
| "fabricator": "0.4.x", | ||
| "fusing": "0.3.x", | ||
| "formidable": "1.0.x", | ||
| "fusing": "1.0.x", | ||
| "json-stringify-safe": "5.0.x", | ||
| "routable": "0.0.x", | ||
| "temper": "0.2.x" | ||
| }, | ||
| "devDependencies": { | ||
| "illuminati": "0.0.x", | ||
| "assume": "0.0.x", | ||
| "coveralls": "2.10.x", | ||
| "istanbul": "0.2.x", | ||
| "mocha": "1.20.x", | ||
| "mocha-lcov-reporter": "0.0.x", | ||
| "primus": "2.4.x", | ||
| "substream": "0.1.x", | ||
| "ws": "0.4.x" | ||
| "assume": "1.1.x", | ||
| "bigpipe": "bigpipe/bigpipe", | ||
| "istanbul": "0.3.x", | ||
| "mocha": "2.1.x", | ||
| "pre-commit": "1.0.x" | ||
| } | ||
| } |
+9
-6
@@ -1,7 +0,10 @@ | ||
| # Pagelet [![Build Status][status]](https://travis-ci.org/bigpipe/pagelet) [![NPM version][npmimgurl]](http://badge.fury.io/js/pagelet) [![Coverage Status][coverage]](http://coveralls.io/r/bigpipe/pagelet?branch=master) | ||
| # Pagelet | ||
| [status]: https://travis-ci.org/bigpipe/pagelet.png | ||
| [npmimgurl]: https://badge.fury.io/js/pagelet.png | ||
| [coverage]: http://coveralls.io/repos/bigpipe/pagelet/badge.png?branch=master | ||
| [![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) | ||
| [version]: http://img.shields.io/npm/v/pagelet.svg?style=flat-square | ||
| [build]: http://img.shields.io/travis/bigpipe/pagelet/master.svg?style=flat-square | ||
| [david]: https://img.shields.io/david/bigpipe/pagelet.svg?style=flat-square | ||
| [cover]: http://img.shields.io/coveralls/bigpipe/pagelet/master.svg?style=flat-square | ||
| ## Installation | ||
@@ -279,4 +282,4 @@ | ||
| Pagelet.extend({ | ||
| if: function conditional(req, next) { | ||
| next(false); | ||
| if: function conditional(req, next) { | ||
| next(false); | ||
| }, | ||
@@ -283,0 +286,0 @@ remove: false |
+73
-174
@@ -5,6 +5,7 @@ describe('Pagelet', function () { | ||
| var Pagelet = require('../').extend({ name: 'test' }) | ||
| , custom = '/unexisting/absolute/path/to/prepend' | ||
| , Temper = require('temper') | ||
| , Pipe = require('bigpipe') | ||
| , assume = require('assume') | ||
| , pagelet | ||
| , P; | ||
| , server = require('http').createServer() | ||
| , pagelet, P; | ||
@@ -15,3 +16,4 @@ // | ||
| // | ||
| var temper = { prefetch: function () {} }; | ||
| var temper = new Temper | ||
| , pipe = new Pipe(server); | ||
@@ -30,3 +32,3 @@ beforeEach(function () { | ||
| pagelet = new P(); | ||
| pagelet = new P; | ||
| }); | ||
@@ -38,10 +40,31 @@ | ||
| it('rendering is asynchronously', function (done) { | ||
| it('rendering is asynchronous', function (done) { | ||
| pagelet.get(pagelet.emits('called')); | ||
| // Listening only till after the event is potentially emitted, will ensure | ||
| // callbacks are called asynchronously by pagelet#render. | ||
| // callbacks are called asynchronously by pagelet.render. | ||
| pagelet.on('called', done); | ||
| }); | ||
| it('can have reference to temper', function () { | ||
| pagelet = new P({ temper: temper }); | ||
| var property = Object.getOwnPropertyDescriptor(pagelet, '_temper'); | ||
| assume(pagelet._temper).to.be.an('object'); | ||
| assume(property.writable).to.equal(true); | ||
| assume(property.enumerable).to.equal(true); | ||
| assume(property.configurable).to.equal(true); | ||
| }); | ||
| it('can have reference to pipe instance', function () { | ||
| pagelet = new P({ pipe: pipe }); | ||
| var property = Object.getOwnPropertyDescriptor(pagelet, '_pipe'); | ||
| assume(pagelet._pipe).to.be.an('object'); | ||
| assume(pagelet._pipe).to.be.instanceof(Pipe); | ||
| assume(property.writable).to.equal(true); | ||
| assume(property.enumerable).to.equal(true); | ||
| assume(property.configurable).to.equal(true); | ||
| }); | ||
| describe('.on', function () { | ||
@@ -61,169 +84,43 @@ it('sets the pathname', function () { | ||
| it('resolves the `error` view'); | ||
| it('resolves the `css` files in to an array'); | ||
| it('resolves the `js` files in to an array'); | ||
| it('resolves the `dependencies` files in to an array'); | ||
| }); | ||
| it('resolves the view', function () { | ||
| assume(P.prototype.view).to.equal('fixtures/view.html'); | ||
| describe('.resolve', function () { | ||
| it('is a function', function () { | ||
| assume(Pagelet.resolve).to.be.a('function'); | ||
| assume(P.resolve).to.be.a('function'); | ||
| assume(Pagelet.resolve).to.equal(P.resolve); | ||
| P.on(module); | ||
| assume(P.prototype.view).to.equal(__dirname +'/fixtures/view.html'); | ||
| }); | ||
| it('will resolve provided property on prototype', function () { | ||
| var result = P.resolve('css'); | ||
| assume(result).to.equal(P); | ||
| assume(P.prototype.css).to.be.an('array'); | ||
| assume(P.prototype.css.length).to.equal(1); | ||
| assume(P.prototype.css[0]).to.equal(__dirname + '/fixtures/style.css'); | ||
| }); | ||
| it('can resolve multiple properties at once', function () { | ||
| P.resolve(['css', 'js']); | ||
| assume(P.prototype.css).to.be.an('array'); | ||
| assume(P.prototype.js).to.be.an('array'); | ||
| assume(P.prototype.css.length).to.equal(1); | ||
| assume(P.prototype.js.length).to.equal(1); | ||
| }); | ||
| it('can be provided with a custom source directory', function () { | ||
| P.resolve('css', custom); | ||
| assume(P.prototype.css[0]).to.equal(custom + '/fixtures/style.css'); | ||
| }); | ||
| it('only resolves local files', function () { | ||
| P.resolve('js', custom); | ||
| assume(P.prototype.js[0]).to.not.include(custom); | ||
| assume(P.prototype.js[0]).to.equal('//cdnjs.cloudflare.com/ajax/libs/d3/3.4.8/d3.min.js'); | ||
| }); | ||
| it('can handle property values that are already an array', function () { | ||
| P.resolve('dependencies', custom); | ||
| assume(P.prototype.dependencies.length).to.equal(2); | ||
| assume(P.prototype.dependencies[0]).to.not.include(custom); | ||
| assume(P.prototype.dependencies[0]).to.equal('http://code.jquery.com/jquery-2.0.0.js'); | ||
| assume(P.prototype.dependencies[1]).to.equal(custom + '/fixtures/custom.js'); | ||
| }); | ||
| it('removes undefined values from the array before processing', function () { | ||
| var Undef = P.extend({ | ||
| dependencies: P.prototype.dependencies.concat( | ||
| undefined | ||
| ) | ||
| }); | ||
| assume(Undef.prototype.dependencies.length).to.equal(3); | ||
| Undef.resolve('dependencies', custom); | ||
| assume(Undef.prototype.dependencies.length).to.equal(2); | ||
| assume(Undef.prototype.dependencies).to.not.include(undefined); | ||
| }); | ||
| it('can be overriden', function () { | ||
| P.resolve = function () { | ||
| throw new Error('fucked'); | ||
| }; | ||
| P.on({}); | ||
| }); | ||
| it('resolves the `error` view'); | ||
| }); | ||
| describe('.optimize', function () { | ||
| it('is a function', function () { | ||
| assume(Pagelet.optimize).to.be.a('function'); | ||
| assume(P.optimize).to.be.a('function'); | ||
| assume(Pagelet.optimize).to.equal(P.optimize); | ||
| describe('.discover', function () { | ||
| it('emits discover and returns immediatly if the parent pagelet has no children', function (done) { | ||
| pagelet.once('discover', done); | ||
| pagelet.discover(); | ||
| }); | ||
| it('uses the supplied temper for prefetching', function (next) { | ||
| var calls = 0; | ||
| P.optimize({ | ||
| temper: { | ||
| prefetch: function () { | ||
| ++calls; | ||
| } | ||
| } | ||
| }, function (err) { | ||
| if (err) return next(err); | ||
| /* Disabled for now, might return before 1.0.0 | ||
| it('initializes pagelets by allocating from the Pagelet.freelist', function (done) { | ||
| var Hero = require(__dirname + '/fixtures/pagelets/hero').optimize(app.temper) | ||
| , Faq = require(__dirname + '/fixtures/pages/faq').extend({ pagelets: [ Hero ] }) | ||
| , pageletFreelist = sinon.spy(Hero.freelist, 'alloc') | ||
| , faq = new Faq(app); | ||
| assume(calls).to.equal(2); | ||
| next(); | ||
| faq.once('discover', function () { | ||
| assume(pageletFreelist).to.be.calledOnce; | ||
| done(); | ||
| }); | ||
| }); | ||
| it('resolves the view', function (next) { | ||
| assume(P.prototype.view).to.equal('fixtures/view.html'); | ||
| P.optimize({}, function () { | ||
| assume(P.prototype.view).to.equal(__dirname +'/fixtures/view.html'); | ||
| next(); | ||
| }); | ||
| }); | ||
| it('prefetches the `view`'); | ||
| it('prefetches the `error` view'); | ||
| it('allows rpc as a string', function (next) { | ||
| var X = P.extend({ | ||
| RPC: 'fixtures, bar', | ||
| fixtures: function () {}, | ||
| bar: function () {} | ||
| }); | ||
| X.optimize({ temper: temper }, function (err) { | ||
| if (err) return next(err); | ||
| assume(X.prototype.RPC).to.be.a('array'); | ||
| assume(X.prototype.RPC).to.have.length(2); | ||
| assume(X.prototype.RPC).to.include('bar'); | ||
| assume(X.prototype.RPC).to.include('fixtures'); | ||
| next(); | ||
| }); | ||
| }); | ||
| it('checks if all rpc functions are available', function (next) { | ||
| var X = P.extend({ | ||
| RPC: 'fixtures, bar', | ||
| bar: function () {} | ||
| }); | ||
| X.optimize({ temper: temper }, function (err) { | ||
| assume(err).to.be.a('error'); | ||
| assume(err.message).to.include('fixtures'); | ||
| next(); | ||
| }); | ||
| }); | ||
| it('allows lowercase rpc', function (next) { | ||
| var X = P.extend({ | ||
| rpc: ['fixtures', 'bar'], | ||
| bar: function () {} | ||
| }); | ||
| X.optimize({ temper: temper }, function (err) { | ||
| assume(err).to.be.a('error'); | ||
| assume(err.message).to.include('fixtures'); | ||
| next(); | ||
| }); | ||
| }); | ||
| faq.discover(); | ||
| });*/ | ||
| }); | ||
| describe('.traverse', function () { | ||
| describe('.children', function () { | ||
| it('is a function', function () { | ||
| assume(Pagelet.traverse).to.be.a('function'); | ||
| assume(P.traverse).to.be.a('function'); | ||
| assume(Pagelet.traverse).to.equal(P.traverse); | ||
| assume(Pagelet.children).to.be.a('function'); | ||
| assume(P.children).to.be.a('function'); | ||
| assume(Pagelet.children).to.equal(P.children); | ||
| }); | ||
| it('returns an array', function () { | ||
| var one = P.traverse() | ||
| var one = P.children() | ||
| , recur = P.extend({ | ||
@@ -233,17 +130,16 @@ pagelets: { | ||
| } | ||
| }).traverse('this one'); | ||
| }).children('this one'); | ||
| assume(one).to.be.an('array'); | ||
| assume(one.length).to.equal(1); | ||
| assume(one.length).to.equal(0); | ||
| assume(recur).to.be.an('array'); | ||
| assume(recur.length).to.equal(2); | ||
| assume(recur.length).to.equal(1); | ||
| }); | ||
| it('will at least return the pagelet', function () { | ||
| var single = P.traverse(); | ||
| it('will only return children of the pagelet', function () { | ||
| var single = P.children(); | ||
| assume(single[0].prototype._parent).to.equal(undefined); | ||
| assume(single[0].prototype.directory).to.equal(__dirname); | ||
| assume(single[0].prototype.view).to.equal('fixtures/view.html'); | ||
| assume(single).to.be.an('array'); | ||
| assume(single.length).to.equal(0); | ||
| }); | ||
@@ -261,9 +157,8 @@ | ||
| } | ||
| }).traverse('multiple'); | ||
| }).children('multiple'); | ||
| assume(recur).is.an('array'); | ||
| assume(recur.length).to.equal(3); | ||
| assume(recur[1].prototype.name).to.equal('child'); | ||
| assume(recur[2].prototype.name).to.equal('another'); | ||
| assume(recur.length).to.equal(2); | ||
| assume(recur[0].prototype.name).to.equal('child'); | ||
| assume(recur[1].prototype.name).to.equal('another'); | ||
| }); | ||
@@ -278,8 +173,12 @@ | ||
| } | ||
| }).traverse('parental'); | ||
| }).children('parental'); | ||
| assume(recur[0].prototype._parent).to.equal(undefined); | ||
| assume(recur[1].prototype._parent).to.equal('parental'); | ||
| assume(recur[0].prototype._parent).to.equal('parental'); | ||
| }); | ||
| }); | ||
| describe('.optimize', function () { | ||
| it('should prepare an async call stack'); | ||
| it('should provide optimizer with Pagelet reference if no transform:before event'); | ||
| }); | ||
| }); |
| describe('Pagelet', function () { | ||
| 'use strict'; | ||
| var Primus = require('primus') | ||
| , assume = require('assume') | ||
| , port = 1024 | ||
| , pagelet | ||
| , primus | ||
| , client | ||
| , http; | ||
| // | ||
| // Pre-configure an Pagelet with some default values so we can test if every | ||
| // property is correctly configured. | ||
| // | ||
| var Pagelet = require('../').extend({ | ||
| name: 'test', | ||
| directory: __dirname, | ||
| view: 'fixtures/view.html' | ||
| }); | ||
| beforeEach(function (next) { | ||
| pagelet = new Pagelet(); | ||
| http = require('http').createServer(); | ||
| primus = new Primus(http, { | ||
| transformer: 'websockets', | ||
| plugin: { | ||
| substream: require('substream') | ||
| } | ||
| }); | ||
| http.port = port++; | ||
| http.listen(http.port, next); | ||
| }); | ||
| describe('.connect', function () { | ||
| it('connects without errors', function (next) { | ||
| assume(pagelet.substream).to.be.a('null'); | ||
| client = primus.on('connection', function (spark) { | ||
| pagelet.connect(spark, function connected(err) { | ||
| if (err) return next(err); | ||
| assume(pagelet.substream).to.not.be.a('null'); | ||
| assume(pagelet.substream.write).to.be.a('function'); | ||
| spark.end(); | ||
| next(); | ||
| }); | ||
| }).Socket('http://localhost:'+ http.port); | ||
| }); | ||
| it('returns an error if unauthorized', function (next) { | ||
| var Authorized = Pagelet.extend({ | ||
| if: function conditional(req, enabled) { | ||
| enabled(false); | ||
| } | ||
| }); | ||
| pagelet = new Authorized(); | ||
| client = primus.on('connection', function (spark) { | ||
| pagelet.connect(spark, function connected(err) { | ||
| if (!err) throw new Error('Shit is fucked, no auth, ERROR'); | ||
| assume(pagelet.substream).to.be.a('null'); | ||
| spark.end(); | ||
| next(); | ||
| }); | ||
| }).Socket('http://localhost:'+ http.port); | ||
| }); | ||
| }); | ||
| }); |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 2 instances in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
65463
26.73%5
-44.44%14
27.27%1383
35.99%556
0.54%4
-20%11
83.33%7
40%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
Updated