UNPKG

@ewizardjs/prerenderer

Version:

Fast, flexible, framework-agnostic prerendering for sites and SPAs.

441 lines (350 loc) 16.6 kB
'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;