UNPKG

8.02 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 || []); // promise so we can handle errors and reject,
77 // instead of throwing what would otherwise be uncaught errors in node process
78
79 return new Promise(async (resolve, reject) => {
80 debuglog('call generateCriticalCssWrapped');
81 let formattedCss;
82 let pagePromise;
83
84 try {
85 pagePromise = (0, _browser.getOpenBrowserPage)();
86 formattedCss = await (0, _core.default)({
87 pagePromise,
88 url: options.url,
89 cssString: options.cssString,
90 width,
91 height,
92 forceInclude,
93 forceExclude,
94 strict: options.strict,
95 userAgent: options.userAgent || DEFAULT_USER_AGENT,
96 renderWaitTime: options.renderWaitTime || DEFAULT_RENDER_WAIT_TIMEOUT,
97 timeout: timeoutWait,
98 pageLoadSkipTimeout: options.pageLoadSkipTimeout,
99 blockJSRequests: typeof options.blockJSRequests !== 'undefined' ? options.blockJSRequests : DEFAULT_BLOCK_JS_REQUESTS,
100 customPageHeaders: options.customPageHeaders,
101 cookies: options.cookies,
102 screenshots: options.screenshots,
103 keepLargerMediaQueries: options.keepLargerMediaQueries,
104 maxElementsToCheckPerSelector: options.maxElementsToCheckPerSelector,
105 // postformatting
106 propertiesToRemove,
107 maxEmbeddedBase64Length: typeof options.maxEmbeddedBase64Length === 'number' ? options.maxEmbeddedBase64Length : DEFAULT_MAX_EMBEDDED_BASE64_LENGTH,
108 debuglog,
109 unstableKeepBrowserAlive: options.unstableKeepBrowserAlive,
110 allowedResponseCode: options.allowedResponseCode,
111 unstableKeepOpenPages: options.unstableKeepOpenPages || _UNSTABLE_KEEP_ALIVE_MAX_KEPT_OPEN_PAGES
112 });
113 } catch (e) {
114 const page = await pagePromise.then(({
115 page
116 }) => page);
117 await (0, _browser.closeBrowserPage)({
118 page,
119 error: e,
120 unstableKeepBrowserAlive: options.unstableKeepBrowserAlive,
121 unstableKeepOpenPages: options.unstableKeepOpenPages
122 });
123 const runningBrowswer = await (0, _browser.browserIsRunning)();
124
125 if (!forceTryRestartBrowser && !runningBrowswer) {
126 debuglog('Browser unexpecedly not opened - crashed? ' + '\nurl: ' + options.url + '\ncss length: ' + options.cssString.length);
127
128 try {
129 await (0, _browser.restartBrowser)({
130 width,
131 height,
132 getBrowser: options.puppeteer && options.puppeteer.getBrowser
133 }); // retry
134
135 resolve(generateCriticalCssWrapped(options, {
136 forceTryRestartBrowser: true
137 }));
138 } catch (e) {
139 reject(e);
140 }
141
142 return;
143 }
144
145 reject(e);
146 return;
147 }
148
149 const page = await pagePromise.then(({
150 page
151 }) => page);
152 await (0, _browser.closeBrowserPage)({
153 page,
154 unstableKeepBrowserAlive: options.unstableKeepBrowserAlive,
155 unstableKeepOpenPages: options.unstableKeepOpenPages
156 });
157 debuglog('generateCriticalCss done');
158
159 if (formattedCss.trim().length === 0) {
160 // TODO: would be good to surface this to user, always
161 debuglog('Note: Generated critical css was empty for URL: ' + options.url);
162 resolve('');
163 return;
164 }
165
166 resolve(formattedCss);
167 });
168};
169
170module.exports = function (options, callback) {
171 process.on('exit', exitHandler);
172 process.on('SIGTERM', exitHandler);
173 process.on('SIGINT', exitHandler);
174 return new Promise(async (resolve, reject) => {
175 (0, _browser.addJob)();
176
177 function cleanupAndExit({
178 returnValue,
179 error = null
180 }) {
181 process.removeListener('exit', exitHandler);
182 process.removeListener('SIGTERM', exitHandler);
183 process.removeListener('SIGINT', exitHandler);
184 (0, _browser.removeJob)();
185 (0, _browser.closeBrowser)({
186 unstableKeepBrowserAlive: options.unstableKeepBrowserAlive
187 }); // still supporting legacy callback way of calling Penthouse
188
189 if (callback) {
190 callback(error, returnValue);
191 return;
192 }
193
194 if (error) {
195 reject(error);
196 } else {
197 resolve(returnValue);
198 }
199 } // support legacy mode of passing in css file path instead of string
200
201
202 if (!options.cssString && options.css) {
203 try {
204 const cssString = await readFilePromise(options.css, 'utf8');
205 options = Object.assign({}, options, {
206 cssString
207 });
208 } catch (err) {
209 debuglog('error reading css file: ' + options.css + ', error: ' + err);
210 cleanupAndExit({
211 error: err
212 });
213 return;
214 }
215 }
216
217 if (!options.cssString) {
218 debuglog('Passed in css is empty');
219 cleanupAndExit({
220 error: new Error('css should not be empty')
221 });
222 return;
223 }
224
225 const width = parseInt(options.width || DEFAULT_VIEWPORT_WIDTH, 10);
226 const height = parseInt(options.height || DEFAULT_VIEWPORT_HEIGHT, 10);
227
228 try {
229 // launch the browser
230 await (0, _browser.launchBrowserIfNeeded)({
231 getBrowser: options.puppeteer && options.puppeteer.getBrowser,
232 width,
233 height
234 });
235 const criticalCss = await generateCriticalCssWrapped(options);
236 cleanupAndExit({
237 returnValue: criticalCss
238 });
239 } catch (err) {
240 cleanupAndExit({
241 error: err
242 });
243 }
244 });
245};
\No newline at end of file