@ewizardjs/prerenderer
Version:
Fast, flexible, framework-agnostic prerendering for sites and SPAs.
441 lines (350 loc) • 16.6 kB
JavaScript
'use strict';
var _regenerator = require('babel-runtime/regenerator');
var _regenerator2 = _interopRequireDefault(_regenerator);
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var promiseLimit = require('promise-limit');
var puppeteer = require('puppeteer');
var getInstance = require('./get-instance');
var _require = require('url'),
URL = _require.URL;
var FONTS = /\.(ttf|eot|otf|woff(2)?)(\?[a-z0-9=&.]+)?$/i;
var waitForRender = function waitForRender(options) {
options = options || {};
return new Promise(function (resolve, reject) {
// Render when an event fires on the document.
if (options.renderAfterDocumentEvent) {
if (window['__PRERENDER_STATUS'] && window['__PRERENDER_STATUS'].__DOCUMENT_EVENT_RESOLVED) resolve();
document.addEventListener(options.renderAfterDocumentEvent, function () {
return resolve();
});
// Render after a certain number of milliseconds.
} else if (options.renderAfterTime) {
setTimeout(function () {
return resolve();
}, options.renderAfterTime);
// Default: Render immediately after page content loads.
} else {
resolve();
}
});
};
var PuppeteerRenderer = function () {
function PuppeteerRenderer(rendererOptions) {
_classCallCheck(this, PuppeteerRenderer);
this._puppeteer = null;
this._rendererOptions = rendererOptions || {};
if (this._rendererOptions.maxConcurrentRoutes == null) this._rendererOptions.maxConcurrentRoutes = 0;
if (this._rendererOptions.preloadFonts) this._fonts = new Set();
if (this._rendererOptions.inject && !this._rendererOptions.injectProperty) {
this._rendererOptions.injectProperty = '__PRERENDER_INJECTED';
}
}
_createClass(PuppeteerRenderer, [{
key: 'initialize',
value: function () {
var _ref = _asyncToGenerator( /*#__PURE__*/_regenerator2.default.mark(function _callee() {
var _rendererOptions$inco, incognitoContext;
return _regenerator2.default.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.prev = 0;
// Workaround for Linux SUID Sandbox issues.
if (process.platform === 'linux') {
if (!this._rendererOptions.args) this._rendererOptions.args = [];
if (this._rendererOptions.args.indexOf('--no-sandbox') === -1) {
this._rendererOptions.args.push('--no-sandbox');
this._rendererOptions.args.push('--disable-setuid-sandbox');
}
}
_context.next = 4;
return getInstance(this._rendererOptions);
case 4:
this._puppeteer = _context.sent;
_rendererOptions$inco = this._rendererOptions.incognitoContext, incognitoContext = _rendererOptions$inco === undefined ? true : _rendererOptions$inco;
if (!incognitoContext) {
_context.next = 12;
break;
}
_context.next = 9;
return this._puppeteer.createIncognitoBrowserContext();
case 9:
_context.t0 = _context.sent;
_context.next = 13;
break;
case 12:
_context.t0 = this._puppeteer;
case 13:
this._context = _context.t0;
_context.next = 21;
break;
case 16:
_context.prev = 16;
_context.t1 = _context['catch'](0);
console.error(_context.t1);
console.error('[Prerenderer - PuppeteerRenderer] Unable to start Puppeteer');
// Re-throw the error so it can be handled further up the chain. Good idea or not?
throw _context.t1;
case 21:
return _context.abrupt('return', this._puppeteer);
case 22:
case 'end':
return _context.stop();
}
}
}, _callee, this, [[0, 16]]);
}));
function initialize() {
return _ref.apply(this, arguments);
}
return initialize;
}()
}, {
key: 'handleRequestInterception',
value: function () {
var _ref2 = _asyncToGenerator( /*#__PURE__*/_regenerator2.default.mark(function _callee2(page, baseURL) {
var _this = this;
return _regenerator2.default.wrap(function _callee2$(_context2) {
while (1) {
switch (_context2.prev = _context2.next) {
case 0:
_context2.next = 2;
return page.setRequestInterception(true);
case 2:
page.on('request', function (req) {
// Skip third party requests if needed.
if (_this._rendererOptions.skipThirdPartyRequests) {
if (!req.url().startsWith(baseURL)) {
req.abort();
return;
}
}
if (_this._rendererOptions.preloadFonts) {
var font = req.url();
if (FONTS.test(font)) {
var _ref3 = new URL(font),
pathname = _ref3.pathname;
_this._fonts.add(pathname);
}
}
req.continue();
});
case 3:
case 'end':
return _context2.stop();
}
}
}, _callee2, this);
}));
function handleRequestInterception(_x, _x2) {
return _ref2.apply(this, arguments);
}
return handleRequestInterception;
}()
}, {
key: 'renderRoutes',
value: function () {
var _ref4 = _asyncToGenerator( /*#__PURE__*/_regenerator2.default.mark(function _callee4(routes, Prerenderer) {
var _this2 = this;
var rootOptions, options, limiter, pagePromises;
return _regenerator2.default.wrap(function _callee4$(_context4) {
while (1) {
switch (_context4.prev = _context4.next) {
case 0:
rootOptions = Prerenderer.getOptions();
options = this._rendererOptions;
limiter = promiseLimit(this._rendererOptions.maxConcurrentRoutes);
pagePromises = Promise.all(routes.map(function (route, index) {
return limiter(_asyncToGenerator( /*#__PURE__*/_regenerator2.default.mark(function _callee3() {
var page, baseURL, navigationOptions, renderAfterElementExists, result;
return _regenerator2.default.wrap(function _callee3$(_context3) {
while (1) {
switch (_context3.prev = _context3.next) {
case 0:
_context3.next = 2;
return _this2._context.newPage();
case 2:
page = _context3.sent;
if (options.consoleHandler) {
page.on('console', function (message) {
return options.consoleHandler(route, message);
});
}
if (!options.inject) {
_context3.next = 7;
break;
}
_context3.next = 7;
return page.evaluateOnNewDocument(`(function () { window['${options.injectProperty}'] = ${JSON.stringify(options.inject)}; })();`);
case 7:
baseURL = `http://localhost:${rootOptions.server.port}`;
// Allow setting viewport widths and such.
if (!options.viewport) {
_context3.next = 11;
break;
}
_context3.next = 11;
return page.setViewport(options.viewport);
case 11:
_context3.next = 13;
return _this2.handleRequestInterception(page, baseURL);
case 13:
// Hack just in-case the document event fires before our main listener is added.
if (options.renderAfterDocumentEvent) {
page.evaluateOnNewDocument(function (options) {
window['__PRERENDER_STATUS'] = {};
document.addEventListener(options.renderAfterDocumentEvent, function () {
window['__PRERENDER_STATUS'].__DOCUMENT_EVENT_RESOLVED = true;
});
}, _this2._rendererOptions);
}
navigationOptions = options.navigationOptions ? _extends({ waituntil: 'networkidle0' }, options.navigationOptions) : { waituntil: 'networkidle0' };
_context3.next = 17;
return page.goto(`${baseURL}${route}`, navigationOptions);
case 17:
// Wait for some specific element exists
renderAfterElementExists = _this2._rendererOptions.renderAfterElementExists;
if (!(renderAfterElementExists && typeof renderAfterElementExists === 'string')) {
_context3.next = 21;
break;
}
_context3.next = 21;
return page.waitForSelector(renderAfterElementExists);
case 21:
_context3.next = 23;
return page.evaluateHandle('document.fonts.ready');
case 23:
_context3.next = 25;
return page.evaluate(waitForRender, _this2._rendererOptions);
case 25:
if (!(_this2._rendererOptions.preloadFonts && _this2._fonts.size > 0)) {
_context3.next = 28;
break;
}
_context3.next = 28;
return _this2.preloadFonts(page);
case 28:
if (!(typeof _this2._rendererOptions.onBeforeDone === 'function')) {
_context3.next = 31;
break;
}
_context3.next = 31;
return _this2._rendererOptions.onBeforeDone(page, route);
case 31:
_context3.t0 = route;
_context3.next = 34;
return page.evaluate('window.location.pathname');
case 34:
_context3.t1 = _context3.sent;
_context3.next = 37;
return page.content();
case 37:
_context3.t2 = _context3.sent;
result = {
originalRoute: _context3.t0,
route: _context3.t1,
html: _context3.t2
};
_context3.next = 41;
return page.close();
case 41:
return _context3.abrupt('return', result);
case 42:
case 'end':
return _context3.stop();
}
}
}, _callee3, _this2);
})));
}));
return _context4.abrupt('return', pagePromises);
case 5:
case 'end':
return _context4.stop();
}
}
}, _callee4, this);
}));
function renderRoutes(_x3, _x4) {
return _ref4.apply(this, arguments);
}
return renderRoutes;
}()
}, {
key: 'preloadFonts',
value: function () {
var _ref6 = _asyncToGenerator( /*#__PURE__*/_regenerator2.default.mark(function _callee5(page) {
var headHandle;
return _regenerator2.default.wrap(function _callee5$(_context5) {
while (1) {
switch (_context5.prev = _context5.next) {
case 0:
_context5.next = 2;
return page.$('head');
case 2:
headHandle = _context5.sent;
_context5.next = 5;
return page.evaluate(function (head, fonts) {
var fontsHTML = fonts.map(function (font) {
return `<link rel="preload" href="${font}" as="font"/>`;
}).join('');
head.insertAdjacentHTML('afterbegin', fontsHTML);
}, headHandle, Array.from(this._fonts));
case 5:
_context5.next = 7;
return headHandle.dispose();
case 7:
case 'end':
return _context5.stop();
}
}
}, _callee5, this);
}));
function preloadFonts(_x5) {
return _ref6.apply(this, arguments);
}
return preloadFonts;
}()
}, {
key: 'destroy',
value: function () {
var _ref7 = _asyncToGenerator( /*#__PURE__*/_regenerator2.default.mark(function _callee6() {
return _regenerator2.default.wrap(function _callee6$(_context6) {
while (1) {
switch (_context6.prev = _context6.next) {
case 0:
_context6.next = 2;
return this._context.close();
case 2:
if (!this._rendererOptions.browserUrl) {
_context6.next = 7;
break;
}
_context6.next = 5;
return this._puppeteer.disconnect();
case 5:
_context6.next = 9;
break;
case 7:
_context6.next = 9;
return this._puppeteer.close();
case 9:
case 'end':
return _context6.stop();
}
}
}, _callee6, this);
}));
function destroy() {
return _ref7.apply(this, arguments);
}
return destroy;
}()
}]);
return PuppeteerRenderer;
}();
module.exports = PuppeteerRenderer;