lib/engines/jsdom.js
var jsdom = require('jsdom');
var _ = require('lodash');
var d = require('domain').create();
function JSDomEngine(config, logger) {
this.config = config;
this.logger = logger;
this.config.engineSettings = {
FetchExternalResources: ['script', 'iframe'],
ProcessExternalResources: ['script', 'iframe'],
SkipExternalResources: this.config.ignoreAssets,
MutationEvents: '2.0',
QuerySelector: false
};
}
/*
* Initialize the page
*/
JSDomEngine.prototype.init = function(appUrl, initCallback, errorCallback, beforeInitCallback) {
var _this = this;
this.initializationCallback = initCallback;
this.hasInitializationCallback = true;
this.errorCallback = errorCallback;
this.beforeInitCallback = beforeInitCallback;
this.contentReadyTimer = null;
d.on('error', function(error) {
_this.logger.log('error', 'JSDOM encountered a fatal error:', error.message);
process.exit(1);
});
d.run(function() {
try {
_this.beforeInitCallback(function() {
jsdom.env({
url: appUrl,
features: _this.config.engineSettings,
done: function(errors, window) {
_this.window = window;
_this.document = window.document;
_this.document.addEventListener('XContentReady', _.bind(_this.onPageReady, _this));
_this.window = _this.document.parentWindow;
_this.window.isPrerender = true;
//_this.window.onerror = this.errorCallback; // Not implemented by JSDOM
_this.window.resizeTo(1024, 768);
_this.window.navigator.mimeTypes = []; // Not implememented by JSDOM
_this.bindConsole();
}
});
});
} catch (error) {
_this.errorCallback(error.message);
}
});
};
/*
* Load a route
*/
JSDomEngine.prototype.loadRoute = function(page, callback) {
this.currentPage = page;
this.pageCallback = callback;
this.hasPageCallback = true;
clearTimeout(this.contentReadyTimer);
var _this = this;
// XXX: JSDOM does not currently support push state so update window.location manually
var urlParts = page.url.split('?');
this.window.location.href = this.config.appUrl.substr(0, this.config.appUrl.length - 1) + urlParts[0];
this.window.location.search = urlParts[1] || '';
d.run(function() {
try {
_this.window.prerenderTransitionEvent.url = page.url;
_this.window.document.dispatchEvent(_this.window.prerenderTransitionEvent);
} catch (error) {
_this.logger.log('error', 'JSDOM encountered an error while loading the route:', error.message);
}
});
};
/*
* Callback handler for when a page finishes loading
*/
JSDomEngine.prototype.onPageReady = function() {
var _this = this;
if (this.hasInitializationCallback) {
this.hasInitializationCallback = false;
this.initializationCallback();
} else {
this.contentReadyTimer = setTimeout(function() {
if (_this.hasPageCallback) {
_this.hasPageCallback = false;
var html = _this.window.document.documentElement.outerHTML;
if (_this.window.document.doctype) {
html = "<!DOCTYPE " + _this.window.document.doctype.name + ">\n" + html;
}
_this.currentPage.statusCode = 200;
_this.currentPage.html = html;
_this.pageCallback(_this.currentPage);
}
}, this.config.contentReadyDelay);
}
};
/*
* Destroy the jsdom document
*/
JSDomEngine.prototype.shutdown = function() {
clearTimeout(this.contentReadyTimer);
this.window.close();
clearInterval(this.errorTimer);
};
/*
* Bind JSDom console logging output to PrerenderLogger debug log
*/
JSDomEngine.prototype.bindConsole = function() {
var _this = this;
var methods = ['log', 'debug', 'info', 'warn', 'error'];
methods.forEach(function(method) {
_this.window.console[method] = function() {
var args = [].slice.call(arguments);
args.unshift('debug', '>>>');
return _this.logger.log.apply(_this.logger, args);
};
});
// Error messages are currently a special case
this.errorTimer = setInterval(_.bind(this.logErrors, this), 2000);
};
/*
* Log script errors
*/
JSDomEngine.prototype.logErrors = function() {
var _this = this;
if (this.document.errors.length > 0) {
this.document.errors.forEach(function(error) {
if (error.message.indexOf('NOT IMPLEMENTED') === -1) {
//console.log(error);
_this.logger.log('error', error.message);
}
});
this.document.errors = [];
}
};
module.exports = JSDomEngine;