zipfworks/ember-prerender

View on GitHub
lib/engines/jsdom.js

Summary

Maintainability
A
2 hrs
Test Coverage
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;