UNPKG

penthouse

Version:

Generate critical path CSS for web pages

229 lines (194 loc) 7.47 kB
"use strict"; var _fs = _interopRequireDefault(require("fs")); var _debug = _interopRequireDefault(require("debug")); var _core = _interopRequireDefault(require("./core")); var _browser = require("./browser"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const debuglog = (0, _debug.default)('penthouse'); const DEFAULT_VIEWPORT_WIDTH = 1300; // px const DEFAULT_VIEWPORT_HEIGHT = 900; // px const DEFAULT_TIMEOUT = 30000; // ms const DEFAULT_MAX_EMBEDDED_BASE64_LENGTH = 1000; // chars const DEFAULT_USER_AGENT = 'Penthouse Critical Path CSS Generator'; const DEFAULT_RENDER_WAIT_TIMEOUT = 100; const DEFAULT_BLOCK_JS_REQUESTS = true; const DEFAULT_PROPERTIES_TO_REMOVE = ['(.*)transition(.*)', 'cursor', 'pointer-events', '(-webkit-)?tap-highlight-color', '(.*)user-select']; const _UNSTABLE_KEEP_ALIVE_MAX_KEPT_OPEN_PAGES = 4; function exitHandler(exitCode) { (0, _browser.closeBrowser)({ forceClose: true }); process.exit(typeof exitCode === 'number' ? exitCode : 0); } function readFilePromise(filepath, encoding) { return new Promise((resolve, reject) => { _fs.default.readFile(filepath, encoding, (err, content) => { if (err) { return reject(err); } resolve(content); }); }); } function prepareForceSelectorsForSerialization(forceSelectors = []) { // need to annotate forceInclude values to allow RegExp to pass through JSON serialization return forceSelectors.map(function (forceSelectorValue) { if (typeof forceSelectorValue === 'object' && forceSelectorValue.constructor.name === 'RegExp') { return { type: 'RegExp', source: forceSelectorValue.source, flags: forceSelectorValue.flags }; } return { value: forceSelectorValue }; }); } // const so not hoisted, so can get regeneratorRuntime inlined above, needed for Node 4 const generateCriticalCssWrapped = async function generateCriticalCssWrapped(options, { forceTryRestartBrowser } = {}) { const width = parseInt(options.width || DEFAULT_VIEWPORT_WIDTH, 10); const height = parseInt(options.height || DEFAULT_VIEWPORT_HEIGHT, 10); const timeoutWait = options.timeout || DEFAULT_TIMEOUT; // Merge properties with default ones const propertiesToRemove = options.propertiesToRemove || DEFAULT_PROPERTIES_TO_REMOVE; // always forceInclude '*', 'html', and 'body' selectors; // yields slight performance improvement const forceInclude = prepareForceSelectorsForSerialization(['*', '*:before', '*:after', 'html', 'body'].concat(options.forceInclude || [])); const forceExclude = prepareForceSelectorsForSerialization(options.forceExclude || []); debuglog('call generateCriticalCssWrapped'); let formattedCss; let pagePromise; try { pagePromise = (0, _browser.getOpenBrowserPage)(); formattedCss = await (0, _core.default)({ pagePromise, url: options.url, pageGotoOptions: options.puppeteer && options.puppeteer.pageGotoOptions || {}, cssString: options.cssString, width, height, forceInclude, forceExclude, strict: options.strict, userAgent: options.userAgent || DEFAULT_USER_AGENT, renderWaitTime: options.renderWaitTime || DEFAULT_RENDER_WAIT_TIMEOUT, timeout: timeoutWait, pageLoadSkipTimeout: options.pageLoadSkipTimeout, blockJSRequests: typeof options.blockJSRequests !== 'undefined' ? options.blockJSRequests : DEFAULT_BLOCK_JS_REQUESTS, customPageHeaders: options.customPageHeaders, cookies: options.cookies, screenshots: options.screenshots, keepLargerMediaQueries: options.keepLargerMediaQueries, maxElementsToCheckPerSelector: options.maxElementsToCheckPerSelector, // postformatting propertiesToRemove, maxEmbeddedBase64Length: typeof options.maxEmbeddedBase64Length === 'number' ? options.maxEmbeddedBase64Length : DEFAULT_MAX_EMBEDDED_BASE64_LENGTH, debuglog, unstableKeepBrowserAlive: options.unstableKeepBrowserAlive, allowedResponseCode: options.allowedResponseCode, unstableKeepOpenPages: options.unstableKeepOpenPages || _UNSTABLE_KEEP_ALIVE_MAX_KEPT_OPEN_PAGES }); } catch (e) { const page = await pagePromise.then(({ page }) => page); await (0, _browser.closeBrowserPage)({ page, error: e, unstableKeepBrowserAlive: options.unstableKeepBrowserAlive, unstableKeepOpenPages: options.unstableKeepOpenPages }); const runningBrowswer = await (0, _browser.browserIsRunning)(); if (!forceTryRestartBrowser && !runningBrowswer) { debuglog('Browser unexpecedly not opened - crashed? ' + '\nurl: ' + options.url + '\ncss length: ' + options.cssString.length); await (0, _browser.restartBrowser)({ width, height, getBrowser: options.puppeteer && options.puppeteer.getBrowser }); // retry return generateCriticalCssWrapped(options, { forceTryRestartBrowser: true }); } throw e; } const page = await pagePromise.then(({ page }) => page); await (0, _browser.closeBrowserPage)({ page, unstableKeepBrowserAlive: options.unstableKeepBrowserAlive, unstableKeepOpenPages: options.unstableKeepOpenPages }); debuglog('generateCriticalCss done'); if (formattedCss.trim().length === 0) { // TODO: would be good to surface this to user, always debuglog('Note: Generated critical css was empty for URL: ' + options.url); return ''; } return formattedCss; }; module.exports = async function (options, callback) { process.on('exit', exitHandler); process.on('SIGTERM', exitHandler); process.on('SIGINT', exitHandler); (0, _browser.addJob)(); function cleanupAndExit({ returnValue, error = null }) { process.removeListener('exit', exitHandler); process.removeListener('SIGTERM', exitHandler); process.removeListener('SIGINT', exitHandler); (0, _browser.removeJob)(); (0, _browser.closeBrowser)({ unstableKeepBrowserAlive: options.unstableKeepBrowserAlive }); // still supporting legacy callback way of calling Penthouse if (callback) { callback(error, returnValue); return; } if (error) { throw error; } else { return returnValue; } } // support legacy mode of passing in css file path instead of string if (!options.cssString && options.css) { try { const cssString = await readFilePromise(options.css, 'utf8'); options = Object.assign({}, options, { cssString }); } catch (err) { debuglog('error reading css file: ' + options.css + ', error: ' + err); return cleanupAndExit({ error: err }); } } if (!options.cssString) { debuglog('Passed in css is empty'); return cleanupAndExit({ error: new Error('css should not be empty') }); } const width = parseInt(options.width || DEFAULT_VIEWPORT_WIDTH, 10); const height = parseInt(options.height || DEFAULT_VIEWPORT_HEIGHT, 10); try { // launch the browser await (0, _browser.launchBrowserIfNeeded)({ getBrowser: options.puppeteer && options.puppeteer.getBrowser, width, height }); const criticalCss = await generateCriticalCssWrapped(options); return cleanupAndExit({ returnValue: criticalCss }); } catch (err) { return cleanupAndExit({ error: err }); } };