Comparing version 0.8.2 to 1.0.0-alpha
1148
index.js
'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 @@ |
{ | ||
"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" | ||
} | ||
} |
@@ -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 |
@@ -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'); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
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
65463
5
14
1383
556
11
3
2
+ Addedasync@0.9.x
+ Addeddemolish@1.0.x
+ Addedeventemitter3@0.1.x
+ Addedformidable@1.0.x
+ Addedroutable@0.0.x
+ Addedasync@0.9.2(transitive)
+ Addeddemolish@1.0.2(transitive)
+ Addedemits@3.0.0(transitive)
+ Addedeventemitter3@0.1.6(transitive)
+ Addedformidable@1.0.17(transitive)
+ Addedfusing@1.0.0(transitive)
+ Addedroutable@0.0.5(transitive)
+ Addedxregexp@2.0.0(transitive)
- Removedfusing@0.3.2(transitive)
Updatedfusing@1.0.x