UNPKG

penthouse

Version:

Generate critical path CSS for web pages

228 lines (193 loc) 6.74 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.addJob = addJob; exports.removeJob = removeJob; exports.launchBrowserIfNeeded = launchBrowserIfNeeded; exports.closeBrowser = closeBrowser; exports.restartBrowser = restartBrowser; exports.browserIsRunning = browserIsRunning; exports.getOpenBrowserPage = getOpenBrowserPage; exports.closeBrowserPage = closeBrowserPage; var _puppeteer = _interopRequireDefault(require("puppeteer")); var _debug = _interopRequireDefault(require("debug")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const debuglog = (0, _debug.default)('penthouse:browser'); // shared between penthouse calls let browser = null; let _browserLaunchPromise = null; const reusableBrowserPages = []; // keep track of when we can close the browser penthouse uses; // kept open by continuous use let ongoingJobs = 0; function addJob() { ongoingJobs = ongoingJobs + 1; } function removeJob() { ongoingJobs = ongoingJobs - 1; } const DEFAULT_PUPPETEER_LAUNCH_ARGS = ['--disable-setuid-sandbox', '--no-sandbox', '--ignore-certificate-errors' // better for Docker: // https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#tips // (however caused memory leaks in Penthouse when testing in Ubuntu, hence disabled) // '--disable-dev-shm-usage' ]; async function launchBrowserIfNeeded({ getBrowser, width, height }) { const usingCustomGetBrowser = getBrowser && typeof getBrowser === 'function'; if (usingCustomGetBrowser && !_browserLaunchPromise) { debuglog('using browser provided via getBrowser option'); _browserLaunchPromise = Promise.resolve(getBrowser()); } if (!_browserLaunchPromise) { if (browser) { // we have already a running browser return; } debuglog('no browser instance, launching new browser..'); _browserLaunchPromise = _puppeteer.default.launch({ args: DEFAULT_PUPPETEER_LAUNCH_ARGS, ignoreHTTPSErrors: true, defaultViewport: { width, height } }); } _browserLaunchPromise.then(async browser => { debuglog('browser ready'); const browserPages = await browser.pages(); if (browserPages.length > 0) { debuglog('re-using the page browser launched with'); browserPages.forEach(Page => { if (!reusableBrowserPages.includes(Page)) { Page.notSetupForPenthouse = true; reusableBrowserPages.push(Page); } else { debuglog('ignoring browser page already inside reusableBrowserPages'); } }); } return browser; }); browser = await _browserLaunchPromise; _browserLaunchPromise = null; } async function closeBrowser({ forceClose, unstableKeepBrowserAlive }) { if (browser && (forceClose || !unstableKeepBrowserAlive)) { if (ongoingJobs > 0) { debuglog('keeping browser open as ongoingJobs: ' + ongoingJobs); } else if (browser && browser.close) { browser.close(); browser = null; _browserLaunchPromise = null; debuglog('closed browser'); } } } async function restartBrowser({ getBrowser, width, height }) { let browserPages; if (browser) { browserPages = await browser.pages(); } debuglog('restartBrowser called' + browser && browserPages && '\n_browserPagesOpen: ' + browserPages.length); // for some reason Chromium is no longer opened; // perhaps it crashed if (_browserLaunchPromise) { // in this case the browser is already restarting await _browserLaunchPromise; // if getBrowser is specified the user is managing the puppeteer browser themselves, // so we do nothing. } else if (!getBrowser) { console.log('now restarting chrome after crash'); browser = null; await launchBrowserIfNeeded({ width, height }); } } async function browserIsRunning() { try { // will throw 'Not opened' error if browser is not running await browser.version(); return true; } catch (e) { return false; } } async function getOpenBrowserPage() { const browserPages = await browser.pages(); // if any re-usable pages to use, avoid unnecessary page open/close calls if (reusableBrowserPages.length > 0) { debuglog('re-using browser page for generateCriticalCss, remaining at: ' + browserPages.length); const reusedPage = reusableBrowserPages.pop(); let reused = true; // if we haven't yet run any penthouse jobs with this page, // don't consider it reused - i.e. it will need to be configured. if (reusedPage.notSetupForPenthouse) { reused = false; // but only once delete reusedPage.notSetupForPenthouse; } return Promise.resolve({ page: reusedPage, reused }); } debuglog('adding browser page for generateCriticalCss, before adding was: ' + browserPages.length); return browser.newPage().then(page => { return { page }; }); } async function closeBrowserPage({ page, error, unstableKeepBrowserAlive, unstableKeepOpenPages }) { if (!browser || !page) { return; } const browserPages = await browser.pages(); debuglog('remove (maybe) browser page for generateCriticalCss, before removing was: ' + browserPages.length); const badErrors = ['Target closed', 'Page crashed']; if (page && !(error && badErrors.some(badError => error.toString().indexOf(badError) > -1))) { // Without try/catch if error penthouse will crash if error here, // and wont restart properly try { // must await here, otherwise will receive errors if closing // browser before page is properly closed, // however in unstableKeepBrowserAlive browser is never closed by penthouse. if (unstableKeepBrowserAlive) { if (unstableKeepOpenPages !== 'all' && browserPages.length > unstableKeepOpenPages) { page.close(); } else { debuglog('saving page for re-use, instead of closing'); if (error) { // When a penthouse job execution errors, // in some conditions when later re-use the page // certain methods don't work, // such as Page.setUserAgent never resolving. // "resetting" the page by navigation to about:blank first fixes this. debuglog('Reset page first..'); await page.goto('about:blank').then(() => { debuglog('... page reset DONE'); }); } reusableBrowserPages.push(page); } } else { debuglog('now try to close browser page'); await page.close(); } } catch (err) { debuglog('failed to close browser page (ignoring)'); } } }