New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

pagelet

Package Overview
Dependencies
Maintainers
5
Versions
38
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

pagelet - npm Package Compare versions

Comparing version 0.8.2 to 1.0.0-alpha

helpers.js

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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc