1 | "use strict";
|
2 |
|
3 | Object.defineProperty(exports, "__esModule", {
|
4 | value: true
|
5 | });
|
6 | exports.default = exports.PAGE_UNLOADED_DURING_EXECUTION_ERROR_MESSAGE = void 0;
|
7 |
|
8 | var _cssTree = _interopRequireDefault(require("css-tree"));
|
9 |
|
10 | var _debug = _interopRequireDefault(require("debug"));
|
11 |
|
12 | var _pruneNonCriticalSelectors = _interopRequireDefault(require("./browser-sandbox/pruneNonCriticalSelectors"));
|
13 |
|
14 | var _replacePageCss = _interopRequireDefault(require("./browser-sandbox/replacePageCss"));
|
15 |
|
16 | var _postformatting = _interopRequireDefault(require("./postformatting"));
|
17 |
|
18 | var _selectorsProfile = _interopRequireDefault(require("./selectors-profile"));
|
19 |
|
20 | var _nonMatchingMediaQueryRemover = _interopRequireDefault(require("./non-matching-media-query-remover"));
|
21 |
|
22 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
23 |
|
24 | function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
|
25 |
|
26 | function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
|
27 |
|
28 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
29 |
|
30 | function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); }
|
31 |
|
32 | function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); }
|
33 |
|
34 | function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); }
|
35 |
|
36 | function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } }
|
37 |
|
38 | const debuglog = (0, _debug.default)('penthouse:core');
|
39 | const PUPPETEER_PAGE_UNLOADED_DURING_EXECUTION_ERROR_REGEX = /(Cannot find context with specified id|Execution context was destroyed)/;
|
40 | const PAGE_UNLOADED_DURING_EXECUTION_ERROR_MESSAGE = 'PAGE_UNLOADED_DURING_EXECUTION: Critical css generation script could not be executed.\n\nThis can happen if Penthouse was killed during execution, OR otherwise most commonly if the page navigates away after load, via setting window.location, meta tag refresh directive or similar. For the critical css generation to work the loaded page must stay: remove any redirects or move them to the server. You can also disable them on your end just for the critical css generation, for example via a query parameter.';
|
41 | exports.PAGE_UNLOADED_DURING_EXECUTION_ERROR_MESSAGE = PAGE_UNLOADED_DURING_EXECUTION_ERROR_MESSAGE;
|
42 |
|
43 | function blockinterceptedRequests(interceptedRequest) {
|
44 | const isJsRequest = /\.js(\?.*)?$/.test(interceptedRequest.url());
|
45 |
|
46 | if (isJsRequest) {
|
47 | interceptedRequest.abort();
|
48 | } else {
|
49 | interceptedRequest.continue();
|
50 | }
|
51 | }
|
52 |
|
53 | function loadPage(page, url, timeout, pageLoadSkipTimeout, allowedResponseCode) {
|
54 | debuglog('page load start');
|
55 | let waitingForPageLoad = true;
|
56 | let loadPagePromise = page.goto(url);
|
57 |
|
58 | if (pageLoadSkipTimeout) {
|
59 | loadPagePromise = Promise.race([loadPagePromise, new Promise(resolve => {
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 | setTimeout(() => {
|
66 | if (waitingForPageLoad) {
|
67 | debuglog('page load waiting ABORTED after ' + pageLoadSkipTimeout / 1000 + 's. ');
|
68 | resolve();
|
69 | }
|
70 | }, pageLoadSkipTimeout);
|
71 | })]);
|
72 | }
|
73 |
|
74 | return loadPagePromise.then(response => {
|
75 | if (typeof allowedResponseCode !== 'undefined') {
|
76 | checkResponseStatus(allowedResponseCode, response);
|
77 | }
|
78 |
|
79 | waitingForPageLoad = false;
|
80 | debuglog('page load DONE');
|
81 | });
|
82 | }
|
83 |
|
84 | function checkResponseStatus(allowedResponseCode, response) {
|
85 | var errorMessage;
|
86 |
|
87 | if (typeof allowedResponseCode === 'number' && response.status() !== allowedResponseCode) {
|
88 | errorMessage = `Server response status ${response.status()} isn't matching allowedResponseCode: ${allowedResponseCode}.`;
|
89 | } else if (typeof allowedResponseCode === 'object' && allowedResponseCode.constructor.name === 'RegExp' && !response.status().toString().match(allowedResponseCode)) {
|
90 | errorMessage = `Server response status ${response.status()} isn't matching allowedResponseCode: ${allowedResponseCode.toString()}.`;
|
91 | } else if (typeof allowedResponseCode === 'function' && !allowedResponseCode.call(this, response)) {
|
92 | errorMessage = `Server response status ${response.status()} isn't matching allowedResponseCode.`;
|
93 | }
|
94 |
|
95 | if (errorMessage) {
|
96 | throw new Error(errorMessage);
|
97 | }
|
98 | }
|
99 |
|
100 | function setupBlockJsRequests(page) {
|
101 | page.on('request', blockinterceptedRequests);
|
102 | return page.setRequestInterception(true);
|
103 | }
|
104 |
|
105 | async function astFromCss({
|
106 | cssString,
|
107 | strict
|
108 | }) {
|
109 |
|
110 | const css = cssString.replace(//g, '\f042');
|
111 | const parsingErrors = [];
|
112 | debuglog('parse ast START');
|
113 |
|
114 | const ast = _cssTree.default.parse(css, {
|
115 | onParseError: error => parsingErrors.push(error.formattedMessage)
|
116 | });
|
117 |
|
118 | debuglog(`parse ast DONE (with ${parsingErrors.length} errors)`);
|
119 |
|
120 | if (parsingErrors.length && strict === true) {
|
121 |
|
122 | const parsingErrorMessage = parsingErrors[0];
|
123 | throw new Error(`AST parser (css-tree) found ${parsingErrors.length} errors in CSS.
|
124 | Breaking because in strict mode.
|
125 | The first error was:
|
126 | ` + parsingErrorMessage);
|
127 | }
|
128 |
|
129 | return ast;
|
130 | }
|
131 |
|
132 | async function preparePage({
|
133 | page,
|
134 | pagePromise,
|
135 | width,
|
136 | height,
|
137 | cookies,
|
138 | userAgent,
|
139 | customPageHeaders,
|
140 | blockJSRequests,
|
141 | cleanupAndExit,
|
142 | getHasExited
|
143 | }) {
|
144 | let reusedPage;
|
145 |
|
146 | try {
|
147 | const pagePromiseResult = await pagePromise;
|
148 | page = pagePromiseResult.page;
|
149 | reusedPage = pagePromiseResult.reused;
|
150 | } catch (e) {
|
151 | debuglog('unexpected: could not get an open browser page' + e);
|
152 | return;
|
153 | }
|
154 |
|
155 |
|
156 |
|
157 | if (getHasExited()) {
|
158 | return;
|
159 | }
|
160 |
|
161 | debuglog('open page ready in browser');
|
162 |
|
163 |
|
164 |
|
165 |
|
166 | let setViewportPromise = Promise.resolve();
|
167 | const currentViewport = page.viewport();
|
168 |
|
169 | if (currentViewport.width !== width || currentViewport.height !== height) {
|
170 | setViewportPromise = page.setViewport({
|
171 | width,
|
172 | height
|
173 | }).then(() => debuglog('viewport size updated'));
|
174 | }
|
175 |
|
176 | const setUserAgentPromise = page.setUserAgent(userAgent).then(() => debuglog('userAgent set'));
|
177 | let setCustomPageHeadersPromise = Promise.resolve();
|
178 |
|
179 | if (customPageHeaders && Object.keys(customPageHeaders).length) {
|
180 | try {
|
181 | setCustomPageHeadersPromise = page.setExtraHTTPHeaders(customPageHeaders).then(() => debuglog('customPageHeaders set:' + JSON.stringify(customPageHeaders)));
|
182 | } catch (e) {
|
183 | debuglog('failed setting extra http headers: ' + e);
|
184 | }
|
185 | }
|
186 |
|
187 | let setCookiesPromise = Promise.resolve();
|
188 |
|
189 | if (cookies) {
|
190 | try {
|
191 | var _page;
|
192 |
|
193 | setCookiesPromise = (_page = page).setCookie.apply(_page, _toConsumableArray(cookies)).then(() => debuglog('cookie(s) set: ' + JSON.stringify(cookies)));
|
194 | } catch (e) {
|
195 | debuglog('failed to set cookies: ' + e);
|
196 | }
|
197 | }
|
198 |
|
199 |
|
200 | if (reusedPage) {
|
201 | return Promise.all([setViewportPromise, setUserAgentPromise, setCustomPageHeadersPromise, setCookiesPromise]).then(() => {
|
202 | debuglog('preparePage DONE');
|
203 | return page;
|
204 | });
|
205 | }
|
206 |
|
207 |
|
208 |
|
209 | page.setDefaultNavigationTimeout(0);
|
210 | let blockJSRequestsPromise;
|
211 |
|
212 | if (blockJSRequests) {
|
213 |
|
214 |
|
215 | blockJSRequestsPromise = Promise.all([page.setJavaScriptEnabled(false), setupBlockJsRequests(page)]).then(() => {
|
216 | debuglog('blocking js requests DONE');
|
217 | });
|
218 | }
|
219 |
|
220 | page.on('error', error => {
|
221 | debuglog('page error: ' + error);
|
222 | cleanupAndExit({
|
223 | error
|
224 | });
|
225 | });
|
226 | page.on('console', msg => {
|
227 | const text = msg.text ? typeof msg.text === 'function' ? msg.text() : msg.text : msg;
|
228 |
|
229 |
|
230 | if (/^debug: /.test(text)) {
|
231 | debuglog(text.replace(/^debug: /, ''));
|
232 | }
|
233 | });
|
234 | debuglog('page event listeners set');
|
235 | return Promise.all([setViewportPromise, setUserAgentPromise, setCustomPageHeadersPromise, setCookiesPromise, blockJSRequestsPromise]).then(() => {
|
236 | debuglog('preparePage DONE');
|
237 | return page;
|
238 | });
|
239 | }
|
240 |
|
241 | async function grabPageScreenshot({
|
242 | type,
|
243 | page,
|
244 | screenshots,
|
245 | screenshotExtension,
|
246 | debuglog
|
247 | }) {
|
248 | const path = screenshots.basePath + `-${type}` + screenshotExtension;
|
249 | debuglog(`take ${type} screenshot, START`);
|
250 | return page.screenshot(_objectSpread({}, screenshots, {
|
251 | path
|
252 | })).then(() => debuglog(`take ${type} screenshot DONE, path: ${path}`));
|
253 | }
|
254 |
|
255 | async function pruneNonCriticalCssLauncher({
|
256 | pagePromise,
|
257 | url,
|
258 | cssString,
|
259 | width,
|
260 | height,
|
261 | forceInclude,
|
262 | forceExclude,
|
263 | strict,
|
264 | userAgent,
|
265 | renderWaitTime,
|
266 | timeout,
|
267 | pageLoadSkipTimeout,
|
268 | blockJSRequests,
|
269 | customPageHeaders,
|
270 | cookies,
|
271 | screenshots,
|
272 | propertiesToRemove,
|
273 | maxEmbeddedBase64Length,
|
274 | keepLargerMediaQueries,
|
275 | maxElementsToCheckPerSelector,
|
276 | unstableKeepBrowserAlive,
|
277 | allowedResponseCode
|
278 | }) {
|
279 | let _hasExited = false;
|
280 |
|
281 | const getHasExited = () => _hasExited;
|
282 |
|
283 | const takeScreenshots = screenshots && screenshots.basePath;
|
284 | const screenshotExtension = takeScreenshots && screenshots.type === 'jpeg' ? '.jpg' : '.png';
|
285 |
|
286 |
|
287 |
|
288 | return new Promise(async (resolve, reject) => {
|
289 |
|
290 | debuglog('Penthouse core start');
|
291 | let page = null;
|
292 | let killTimeout = null;
|
293 |
|
294 | async function cleanupAndExit({
|
295 | error,
|
296 | returnValue
|
297 | }) {
|
298 | if (_hasExited) {
|
299 | return;
|
300 | }
|
301 |
|
302 | debuglog('cleanupAndExit');
|
303 | _hasExited = true;
|
304 | clearTimeout(killTimeout);
|
305 |
|
306 | if (error) {
|
307 | return reject(error);
|
308 | }
|
309 |
|
310 | if (page) {
|
311 | const resetPromises = [];
|
312 |
|
313 |
|
314 | if (customPageHeaders && Object.keys(customPageHeaders).length) {
|
315 | try {
|
316 | resetPromises.push(page.setExtraHTTPHeaders({}).then(() => debuglog('customPageHeaders reset')));
|
317 | } catch (e) {
|
318 | debuglog('failed resetting extra http headers: ' + e);
|
319 | }
|
320 | }
|
321 |
|
322 |
|
323 | if (cookies && cookies.length) {
|
324 | try {
|
325 | var _page2;
|
326 |
|
327 | resetPromises.push((_page2 = page).deleteCookie.apply(_page2, _toConsumableArray(cookies)).then(() => debuglog('cookie(s) reset: ')));
|
328 | } catch (e) {
|
329 | debuglog('failed to reset cookies: ' + e);
|
330 | }
|
331 | }
|
332 |
|
333 | await Promise.all(resetPromises);
|
334 | }
|
335 |
|
336 | return resolve(returnValue);
|
337 | }
|
338 |
|
339 | killTimeout = setTimeout(() => {
|
340 | cleanupAndExit({
|
341 | error: new Error('Penthouse timed out after ' + timeout / 1000 + 's. ')
|
342 | });
|
343 | }, timeout);
|
344 |
|
345 | const updatedPagePromise = preparePage({
|
346 | page,
|
347 | pagePromise,
|
348 | width,
|
349 | height,
|
350 | userAgent,
|
351 | cookies,
|
352 | customPageHeaders,
|
353 | blockJSRequests,
|
354 | cleanupAndExit,
|
355 | getHasExited
|
356 | });
|
357 |
|
358 |
|
359 | let ast;
|
360 |
|
361 | try {
|
362 | ast = await astFromCss({
|
363 | cssString,
|
364 | strict
|
365 | });
|
366 | } catch (e) {
|
367 | cleanupAndExit({
|
368 | error: e
|
369 | });
|
370 | return;
|
371 | }
|
372 |
|
373 |
|
374 |
|
375 |
|
376 |
|
377 | (0, _nonMatchingMediaQueryRemover.default)(ast, width, height, keepLargerMediaQueries);
|
378 | debuglog('stripped out non matching media queries');
|
379 |
|
380 | page = await updatedPagePromise;
|
381 |
|
382 | if (!page) {
|
383 | cleanupAndExit({
|
384 | error: 'Could not open page in browser'
|
385 | });
|
386 | return;
|
387 | }
|
388 |
|
389 |
|
390 | const loadPagePromise = loadPage(page, url, timeout, pageLoadSkipTimeout, allowedResponseCode);
|
391 |
|
392 | debuglog('turn css to formatted selectorlist START');
|
393 | const buildSelectorProfilePromise = (0, _selectorsProfile.default)(ast, forceInclude && forceInclude.length ? forceInclude : null, forceExclude && forceExclude.length ? forceExclude : null).then(res => {
|
394 | debuglog('turn css to formatted selectorlist DONE');
|
395 | return res;
|
396 | });
|
397 |
|
398 | try {
|
399 | await loadPagePromise;
|
400 | } catch (e) {
|
401 | cleanupAndExit({
|
402 | error: e
|
403 | });
|
404 | return;
|
405 | }
|
406 |
|
407 | if (!page) {
|
408 |
|
409 | debuglog('page load TIMED OUT');
|
410 | cleanupAndExit({
|
411 | error: new Error('Page load timed out')
|
412 | });
|
413 | return;
|
414 | }
|
415 |
|
416 | if (_hasExited) return;
|
417 |
|
418 |
|
419 |
|
420 |
|
421 |
|
422 |
|
423 |
|
424 |
|
425 |
|
426 |
|
427 |
|
428 |
|
429 |
|
430 |
|
431 |
|
432 | await new Promise(resolve => {
|
433 | setTimeout(() => {
|
434 | debuglog('waited for renderWaitTime: ' + renderWaitTime);
|
435 | resolve();
|
436 | }, renderWaitTime);
|
437 | });
|
438 |
|
439 | const beforeScreenshotPromise = takeScreenshots ? grabPageScreenshot({
|
440 | type: 'before',
|
441 | page,
|
442 | screenshots,
|
443 | screenshotExtension,
|
444 | debuglog
|
445 | }) : Promise.resolve();
|
446 |
|
447 |
|
448 | const {
|
449 | selectors,
|
450 | selectorNodeMap
|
451 | } = await buildSelectorProfilePromise;
|
452 |
|
453 | if (getHasExited()) {
|
454 | return;
|
455 | }
|
456 |
|
457 |
|
458 | let criticalSelectors;
|
459 |
|
460 | try {
|
461 | criticalSelectors = await page.evaluate(_pruneNonCriticalSelectors.default, {
|
462 | selectors,
|
463 | renderWaitTime,
|
464 | maxElementsToCheckPerSelector
|
465 | }).then(criticalSelectors => {
|
466 | debuglog('pruneNonCriticalSelectors done');
|
467 | return criticalSelectors;
|
468 | });
|
469 | } catch (err) {
|
470 | debuglog('pruneNonCriticalSelector threw an error: ' + err);
|
471 | const errorDueToPageUnloaded = PUPPETEER_PAGE_UNLOADED_DURING_EXECUTION_ERROR_REGEX.test(err);
|
472 | cleanupAndExit({
|
473 | error: errorDueToPageUnloaded ? new Error(PAGE_UNLOADED_DURING_EXECUTION_ERROR_MESSAGE) : err
|
474 | });
|
475 | return;
|
476 | }
|
477 |
|
478 | if (getHasExited()) {
|
479 | return;
|
480 | }
|
481 |
|
482 |
|
483 | debuglog('AST cleanup START');
|
484 |
|
485 | (0, _postformatting.default)({
|
486 | ast,
|
487 | selectorNodeMap,
|
488 | criticalSelectors,
|
489 | propertiesToRemove,
|
490 | maxEmbeddedBase64Length
|
491 | });
|
492 | debuglog('AST cleanup DONE');
|
493 |
|
494 | const css = _cssTree.default.generate(ast);
|
495 |
|
496 | debuglog('generated CSS from AST');
|
497 |
|
498 | if (takeScreenshots) {
|
499 |
|
500 | await beforeScreenshotPromise;
|
501 | debuglog('inline critical styles for after screenshot');
|
502 | await page.evaluate(_replacePageCss.default, {
|
503 | css
|
504 | }).then(() => {
|
505 | return grabPageScreenshot({
|
506 | type: 'after',
|
507 | page,
|
508 | screenshots,
|
509 | screenshotExtension,
|
510 | debuglog
|
511 | });
|
512 | });
|
513 | }
|
514 |
|
515 | debuglog('generateCriticalCss DONE');
|
516 | cleanupAndExit({
|
517 | returnValue: css
|
518 | });
|
519 | });
|
520 | }
|
521 |
|
522 | var _default = pruneNonCriticalCssLauncher;
|
523 | exports.default = _default; |
\ | No newline at end of file |