UNPKG

7.61 kBJavaScriptView Raw
1'use strict';
2
3const os = require('os');
4const path = require('path');
5const url = require('url');
6const {assign, flatten, invokeMap, uniq} = require('lodash');
7const penthouse = require('penthouse');
8const CleanCSS = require('clean-css');
9const filterCss = require('filter-css');
10const oust = require('oust');
11const postcss = require('postcss');
12const imageInliner = require('postcss-image-inliner');
13const Bluebird = require('bluebird');
14const debug = require('debug')('critical:core');
15
16const file = require('./file-helper');
17
18/**
19 * Returns a string of combined and deduped css rules.
20 * @param cssArray
21 * @returns {String}
22 */
23function combineCss(cssArray) {
24 if (cssArray.length === 1) {
25 return cssArray[0].toString();
26 }
27
28 return new CleanCSS({
29 level: {
30 1: {
31 all: true
32 },
33 2: {
34 all: false,
35 removeDuplicateFontRules: true,
36 removeDuplicateMediaBlocks: true,
37 removeDuplicateRules: true,
38 removeEmpty: true,
39 mergeMedia: true
40 }
41 }
42 }).minify(
43 invokeMap(cssArray, 'toString').join(' ')
44 ).styles;
45}
46
47/**
48 * Append stylesheets to result
49 * @param opts
50 * @returns {function}
51 */
52function appendStylesheets(opts) {
53 return htmlfile => {
54 // Consider opts.css and map to array if it isn't one
55 if (opts.css) {
56 const css = Array.isArray(opts.css) ? opts.css : [opts.css];
57 return Bluebird.map(css, stylesheet => file.assertLocal(stylesheet, opts)).then(stylesheets => {
58 htmlfile.stylesheets = stylesheets;
59 return htmlfile;
60 });
61 }
62
63 // Oust extracts a list of your stylesheets
64 let stylesheets = flatten([
65 oust.raw(htmlfile.contents.toString(), 'stylesheets'),
66 oust.raw(htmlfile.contents.toString(), 'preload')
67 ]).filter(link => link.$el.attr('media') !== 'print' && Boolean(link.value)).map(link => link.value);
68
69 stylesheets = uniq(stylesheets).map(file.resourcePath(htmlfile, opts));
70 debug('appendStylesheets', stylesheets);
71
72 if (stylesheets.length === 0) {
73 return Promise.reject(new Error('No usable stylesheets found in html source. Try to specify the stylesheets manually.'));
74 }
75
76 return Bluebird.map(stylesheets, stylesheet => file.assertLocal(stylesheet, opts)).then(stylesheets => {
77 htmlfile.stylesheets = stylesheets;
78 return htmlfile;
79 });
80 };
81}
82
83/**
84 * Inline images using postcss-image-inliner
85 * @param opts
86 * @returns {function}
87 */
88function inlineImages(opts) {
89 return vinyl => {
90 if (opts.inlineImages) {
91 const assetPaths = opts.assetPaths || [];
92
93 // Add some suitable fallbacks for convinience if nothing is set.
94 // Otherwise don't add them to keep the user in control
95 if (assetPaths.length === 0) {
96 assetPaths.push(path.dirname(vinyl.path));
97 // Add domain as asset source for external domains
98 if (file.isExternal(opts.src)) {
99 // eslint-disable-next-line node/no-deprecated-api
100 const urlObj = url.parse(opts.src);
101 const domain = `${urlObj.protocol}//${urlObj.host}`;
102 assetPaths.push(domain, domain + path.dirname(urlObj.pathname));
103 }
104
105 if (opts.base) {
106 assetPaths.push(opts.base);
107 }
108 }
109
110 const inlineOptions = {
111 assetPaths: uniq(assetPaths),
112 maxFileSize: opts.maxImageFileSize || 10240
113 };
114 debug('inlineImages', inlineOptions);
115 return postcss([imageInliner(inlineOptions)])
116 .process(vinyl.contents.toString('utf8'), {from: undefined})
117 .then(contents => {
118 vinyl.contents = Buffer.from(contents.css);
119 return vinyl;
120 });
121 }
122
123 return vinyl;
124 };
125}
126
127/**
128 * Helper function create vinyl objects
129 * @param opts
130 * @returns {function}
131 */
132function vinylize(opts) {
133 return filepath => {
134 if (filepath._isVinyl) {
135 return filepath;
136 }
137
138 debug('vinylize', path.resolve(filepath));
139 return file.getVinylPromise({
140 src: path.resolve(filepath),
141 base: opts.base
142 });
143 };
144}
145
146/**
147 * Read css source, inline images and normalize relative paths
148 * @param opts
149 * @returns {function}
150 */
151function processStylesheets(opts) {
152 return htmlfile => {
153 debug('processStylesheets', htmlfile.stylesheets);
154 return Bluebird.map(htmlfile.stylesheets, vinylize(opts))
155 .map(inlineImages(opts))
156 .map(file.replaceAssetPaths(htmlfile, opts))
157 .reduce((total, stylesheet) => {
158 return total + os.EOL + stylesheet.contents.toString('utf8');
159 }, '')
160 .then(css => {
161 htmlfile.cssString = css;
162 return htmlfile;
163 });
164 };
165}
166
167/**
168 * Fire up a server as pentouse doesn't like filesystem paths on windows
169 * and let pentouse compute the critical css for us
170 * @param dimensions
171 * @param {object} opts Options passed to critical
172 * @returns {function}
173 */
174function computeCritical(dimensions, opts) {
175 return htmlfile => {
176 debug(`Processing: ${htmlfile.path} [${dimensions.width}x${dimensions.height}]`);
177 const penthouseOpts = assign({}, opts.penthouse, {
178 url: file.getPenthouseUrl(opts, htmlfile),
179 cssString: htmlfile.cssString,
180 width: dimensions.width,
181 height: dimensions.height,
182 userAgent: opts.userAgent
183 });
184
185 if (opts.user && opts.pass) {
186 penthouseOpts.customPageHeaders = {Authorization: `Basic ${file.token(opts.user, opts.pass)}`};
187 }
188
189 return penthouse(penthouseOpts);
190 };
191}
192
193/**
194 * Critical path CSS generation
195 * @param {object} opts Options
196 * @accepts src, base, width, height, dimensions, dest
197 * @return {Promise}
198 */
199function generate(opts) {
200 const cleanCSS = new CleanCSS();
201 opts = opts || {};
202
203 if (!opts.src && !opts.html) {
204 return Bluebird.reject(new Error('A valid source is required.'));
205 }
206
207 if (!opts.dimensions) {
208 opts.dimensions = [{
209 height: opts.height || 900,
210 width: opts.width || 1300
211 }];
212 }
213
214 debug('Start with the following options');
215 debug(opts);
216
217 return Bluebird.map(opts.dimensions, dimensions => {
218 // Use content to fetch used css files
219 return file.getVinylPromise(opts)
220 .then(appendStylesheets(opts))
221 .then(processStylesheets(opts))
222 .then(computeCritical(dimensions, opts));
223 }).then(criticalCSS => {
224 criticalCSS = combineCss(criticalCSS);
225
226 if (opts.ignore) {
227 debug('generate', 'Applying filter', opts.ignore);
228 criticalCSS = filterCss(criticalCSS, opts.ignore, opts.ignoreOptions || {});
229 }
230
231 debug('generate', 'Minify css');
232 criticalCSS = cleanCSS.minify(criticalCSS).styles;
233
234 debug('generate', 'Done');
235 return criticalCSS;
236 }).catch(error => {
237 if (error.message.startsWith('PAGE_UNLOADED_DURING_EXECUTION')) {
238 return '';
239 }
240
241 return Promise.reject(error);
242 });
243}
244
245exports.appendStylesheets = appendStylesheets;
246exports.generate = generate;