UNPKG

7.39 kBJavaScriptView Raw
1"use strict";
2
3var _fs = _interopRequireDefault(require("fs"));
4
5var _debug = _interopRequireDefault(require("debug"));
6
7var _core = _interopRequireDefault(require("./core"));
8
9var _browser = require("./browser");
10
11function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
12
13const debuglog = (0, _debug.default)('penthouse');
14const DEFAULT_VIEWPORT_WIDTH = 1300; // px
15
16const DEFAULT_VIEWPORT_HEIGHT = 900; // px
17
18const DEFAULT_TIMEOUT = 30000; // ms
19
20const DEFAULT_MAX_EMBEDDED_BASE64_LENGTH = 1000; // chars
21
22const DEFAULT_USER_AGENT = 'Penthouse Critical Path CSS Generator';
23const DEFAULT_RENDER_WAIT_TIMEOUT = 100;
24const DEFAULT_BLOCK_JS_REQUESTS = true;
25const DEFAULT_PROPERTIES_TO_REMOVE = ['(.*)transition(.*)', 'cursor', 'pointer-events', '(-webkit-)?tap-highlight-color', '(.*)user-select'];
26const _UNSTABLE_KEEP_ALIVE_MAX_KEPT_OPEN_PAGES = 4;
27
28function exitHandler(exitCode) {
29 (0, _browser.closeBrowser)({
30 forceClose: true
31 });
32 process.exit(typeof exitCode === 'number' ? exitCode : 0);
33}
34
35function readFilePromise(filepath, encoding) {
36 return new Promise((resolve, reject) => {
37 _fs.default.readFile(filepath, encoding, (err, content) => {
38 if (err) {
39 return reject(err);
40 }
41
42 resolve(content);
43 });
44 });
45}
46
47function prepareForceSelectorsForSerialization(forceSelectors = []) {
48 // need to annotate forceInclude values to allow RegExp to pass through JSON serialization
49 return forceSelectors.map(function (forceSelectorValue) {
50 if (typeof forceSelectorValue === 'object' && forceSelectorValue.constructor.name === 'RegExp') {
51 return {
52 type: 'RegExp',
53 source: forceSelectorValue.source,
54 flags: forceSelectorValue.flags
55 };
56 }
57
58 return {
59 value: forceSelectorValue
60 };
61 });
62} // const so not hoisted, so can get regeneratorRuntime inlined above, needed for Node 4
63
64
65const generateCriticalCssWrapped = async function generateCriticalCssWrapped(options, {
66 forceTryRestartBrowser
67} = {}) {
68 const width = parseInt(options.width || DEFAULT_VIEWPORT_WIDTH, 10);
69 const height = parseInt(options.height || DEFAULT_VIEWPORT_HEIGHT, 10);
70 const timeoutWait = options.timeout || DEFAULT_TIMEOUT; // Merge properties with default ones
71
72 const propertiesToRemove = options.propertiesToRemove || DEFAULT_PROPERTIES_TO_REMOVE; // always forceInclude '*', 'html', and 'body' selectors;
73 // yields slight performance improvement
74
75 const forceInclude = prepareForceSelectorsForSerialization(['*', '*:before', '*:after', 'html', 'body'].concat(options.forceInclude || []));
76 const forceExclude = prepareForceSelectorsForSerialization(options.forceExclude || []);
77 debuglog('call generateCriticalCssWrapped');
78 let formattedCss;
79 let pagePromise;
80
81 try {
82 pagePromise = (0, _browser.getOpenBrowserPage)();
83 formattedCss = await (0, _core.default)({
84 pagePromise,
85 url: options.url,
86 cssString: options.cssString,
87 width,
88 height,
89 forceInclude,
90 forceExclude,
91 strict: options.strict,
92 userAgent: options.userAgent || DEFAULT_USER_AGENT,
93 renderWaitTime: options.renderWaitTime || DEFAULT_RENDER_WAIT_TIMEOUT,
94 timeout: timeoutWait,
95 pageLoadSkipTimeout: options.pageLoadSkipTimeout,
96 blockJSRequests: typeof options.blockJSRequests !== 'undefined' ? options.blockJSRequests : DEFAULT_BLOCK_JS_REQUESTS,
97 customPageHeaders: options.customPageHeaders,
98 cookies: options.cookies,
99 screenshots: options.screenshots,
100 keepLargerMediaQueries: options.keepLargerMediaQueries,
101 maxElementsToCheckPerSelector: options.maxElementsToCheckPerSelector,
102 // postformatting
103 propertiesToRemove,
104 maxEmbeddedBase64Length: typeof options.maxEmbeddedBase64Length === 'number' ? options.maxEmbeddedBase64Length : DEFAULT_MAX_EMBEDDED_BASE64_LENGTH,
105 debuglog,
106 unstableKeepBrowserAlive: options.unstableKeepBrowserAlive,
107 allowedResponseCode: options.allowedResponseCode,
108 unstableKeepOpenPages: options.unstableKeepOpenPages || _UNSTABLE_KEEP_ALIVE_MAX_KEPT_OPEN_PAGES
109 });
110 } catch (e) {
111 const page = await pagePromise.then(({
112 page
113 }) => page);
114 await (0, _browser.closeBrowserPage)({
115 page,
116 error: e,
117 unstableKeepBrowserAlive: options.unstableKeepBrowserAlive,
118 unstableKeepOpenPages: options.unstableKeepOpenPages
119 });
120 const runningBrowswer = await (0, _browser.browserIsRunning)();
121
122 if (!forceTryRestartBrowser && !runningBrowswer) {
123 debuglog('Browser unexpecedly not opened - crashed? ' + '\nurl: ' + options.url + '\ncss length: ' + options.cssString.length);
124 await (0, _browser.restartBrowser)({
125 width,
126 height,
127 getBrowser: options.puppeteer && options.puppeteer.getBrowser
128 }); // retry
129
130 return generateCriticalCssWrapped(options, {
131 forceTryRestartBrowser: true
132 });
133 }
134
135 throw e;
136 }
137
138 const page = await pagePromise.then(({
139 page
140 }) => page);
141 await (0, _browser.closeBrowserPage)({
142 page,
143 unstableKeepBrowserAlive: options.unstableKeepBrowserAlive,
144 unstableKeepOpenPages: options.unstableKeepOpenPages
145 });
146 debuglog('generateCriticalCss done');
147
148 if (formattedCss.trim().length === 0) {
149 // TODO: would be good to surface this to user, always
150 debuglog('Note: Generated critical css was empty for URL: ' + options.url);
151 return '';
152 }
153
154 return formattedCss;
155};
156
157module.exports = async function (options, callback) {
158 process.on('exit', exitHandler);
159 process.on('SIGTERM', exitHandler);
160 process.on('SIGINT', exitHandler);
161 (0, _browser.addJob)();
162
163 function cleanupAndExit({
164 returnValue,
165 error = null
166 }) {
167 process.removeListener('exit', exitHandler);
168 process.removeListener('SIGTERM', exitHandler);
169 process.removeListener('SIGINT', exitHandler);
170 (0, _browser.removeJob)();
171 (0, _browser.closeBrowser)({
172 unstableKeepBrowserAlive: options.unstableKeepBrowserAlive
173 }); // still supporting legacy callback way of calling Penthouse
174
175 if (callback) {
176 callback(error, returnValue);
177 return;
178 }
179
180 if (error) {
181 throw error;
182 } else {
183 return returnValue;
184 }
185 } // support legacy mode of passing in css file path instead of string
186
187
188 if (!options.cssString && options.css) {
189 try {
190 const cssString = await readFilePromise(options.css, 'utf8');
191 options = Object.assign({}, options, {
192 cssString
193 });
194 } catch (err) {
195 debuglog('error reading css file: ' + options.css + ', error: ' + err);
196 return cleanupAndExit({
197 error: err
198 });
199 }
200 }
201
202 if (!options.cssString) {
203 debuglog('Passed in css is empty');
204 return cleanupAndExit({
205 error: new Error('css should not be empty')
206 });
207 }
208
209 const width = parseInt(options.width || DEFAULT_VIEWPORT_WIDTH, 10);
210 const height = parseInt(options.height || DEFAULT_VIEWPORT_HEIGHT, 10);
211
212 try {
213 // launch the browser
214 await (0, _browser.launchBrowserIfNeeded)({
215 getBrowser: options.puppeteer && options.puppeteer.getBrowser,
216 width,
217 height
218 });
219 const criticalCss = await generateCriticalCssWrapped(options);
220 return cleanupAndExit({
221 returnValue: criticalCss
222 });
223 } catch (err) {
224 return cleanupAndExit({
225 error: err
226 });
227 }
228};
\No newline at end of file