• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

addyosmani / critical / 25976250177

16 May 2026 11:53PM UTC coverage: 90.27% (+1.6%) from 88.691%
25976250177

Pull #620

github

bezoerb
fix: prefix unused parameters with underscore
Pull Request #620: vite+ & dependency update

535 of 611 branches covered (87.56%)

Branch coverage included in aggregate %.

163 of 176 new or added lines in 7 files covered. (92.61%)

1 existing line in 1 file now uncovered.

634 of 684 relevant lines covered (92.69%)

559.39 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.03
/src/core.js
1
import { EOL } from "node:os";
2
import { Buffer } from "node:buffer";
3
import process from "node:process";
4
import path from "node:path";
5
import pico from "picocolors";
6
import CleanCSS from "clean-css";
7
import { invokeMap } from "lodash-es";
8
import pAll from "p-all";
9
import debugBase from "debug";
10
import postcss from "postcss";
11
import discard from "postcss-discard";
12
import imageInliner from "postcss-image-inliner";
13
import penthouse, { PAGE_UNLOADED_DURING_EXECUTION_ERROR_MESSAGE } from "penthouse-esm";
14
import { inline as inlineCritical } from "inline-critical";
15
import { removeDuplicateStyles } from "inline-critical/css";
16
import parseCssUrls from "css-url-parser";
17
import { reduceAsync } from "./array.js";
18
import { NoCssError } from "./errors.js";
19
import {
20
  getDocument,
21
  getDocumentFromSource,
22
  token,
23
  getAssetPaths,
24
  isRemote,
25
  normalizePath,
26
} from "./file.js";
27

28
const debug = debugBase("critical:core");
12✔
29

30
/**
31
 * Returns a string of combined and deduped css rules.
32
 * @param {array} cssArray Array with css strings
33
 * @returns {String} combined and deduped css rules
34
 */
35
function combineCss(cssArray) {
36
  if (cssArray.length === 1) {
388✔
37
    return cssArray[0].toString();
324✔
38
  }
39

40
  return new CleanCSS().minify(invokeMap(cssArray, "toString").join(" ")).styles;
64✔
41
}
42

43
/**
44
 * Let penthouse compute the critical css
45
 * @param {vinyl} document Vinyl representation of the HTML document
46
 * @param {object} options Options passed to critical
47
 * @returns {string} Critical css for various dimensions combined and deduped
48
 */
49
function callPenthouse(document, options) {
50
  const { dimensions, width, height, userAgent, user, pass, penthouse: params = {} } = options;
396✔
51
  const { customPageHeaders = {} } = params;
396✔
52
  const { css: cssString, url } = document;
396✔
53
  const config = { ...params, cssString, url };
396✔
54
  // Dimensions need to be sorted from small to wide. Otherwise the order gets corrupted
55
  const sizes = Array.isArray(dimensions)
396✔
56
    ? [...dimensions].sort((a, b) => (a.width || 0) - (b.width || 0))
212!
57
    : [{ width, height }];
58

59
  if (userAgent) {
396✔
60
    config.userAgent = userAgent;
4✔
61
  }
62

63
  if (user && pass) {
396!
NEW
64
    config.customPageHeaders = {
×
65
      ...customPageHeaders,
66
      Authorization: `Basic ${token(user, pass)}`,
67
    };
68
  }
69

70
  return sizes.map(({ width, height }) => () => {
520✔
71
    const result = penthouse({ ...config, width, height });
520✔
72
    debug("Call penthouse with:", {
520✔
73
      ...config,
74
      width,
75
      height,
76
      cssString: `${(cssString || "").slice(0, 10)} ... ${(cssString || "").slice(-10)}`,
1,040!
77
    });
78

79
    return result;
520✔
80
  });
81
}
82

83
/**
84
 * Critical path CSS generation
85
 * @param  {object} options Options
86
 * @accepts src, base, width, height, dimensions, dest
87
 * @return {Promise<object>} Object with critical css & html
88
 */
89
export async function create(options = {}) {
420✔
90
  const {
91
    base,
92
    src,
93
    html,
94
    inline,
95
    ignore,
96
    extract,
97
    target = {},
420✔
98
    inlineImages,
99
    maxImageFileSize,
100
    postcss: postProcess = [],
420✔
101
    strict,
102
    cleanCSS: cleanCSSOptions,
103
    concurrency = Number.POSITIVE_INFINITY,
420✔
104
    assetPaths = [],
420✔
105
  } = options;
420✔
106

107
  // Create vinyl representation for the document with normalized filepath and normalized styles
108
  const document = src
420✔
109
    ? await getDocument(src, options)
110
    : await getDocumentFromSource(html, options);
111

112
  if (!document.css || !document.css.toString()) {
40✔
113
    if (strict) {
20✔
114
      throw new NoCssError();
4✔
115
    }
116

117
    return {
16✔
118
      css: "",
119
      html: document.contents.toString(),
120
    };
121
  }
122

123
  // Generate critical css
124
  let criticalCSS;
125
  try {
396✔
126
    const tasks = callPenthouse(document, options);
396✔
127
    const criticalStyles = await pAll(tasks, { concurrency });
396✔
128
    criticalCSS = combineCss(criticalStyles);
388✔
129
  } catch (error) {
130
    if (error.message === PAGE_UNLOADED_DURING_EXECUTION_ERROR_MESSAGE) {
8✔
131
      process.stderr.write(pico.yellow(PAGE_UNLOADED_DURING_EXECUTION_ERROR_MESSAGE) + EOL);
4✔
132
      return {
4✔
133
        css: "",
134
        html: document.contents.toString(),
135
      };
136
    }
137

138
    throw error;
4✔
139
  }
140

141
  // Add postprocess configuration
142
  if (ignore) {
388✔
143
    postProcess.push(discard(ignore));
32✔
144
  }
145

146
  if (inlineImages) {
388✔
147
    const refAssets = [...parseCssUrls(criticalCSS), ...document.stylesheets];
44✔
148
    const refAssetPaths = refAssets.reduce((res, file) => [...res, path.dirname(file)], []);
88✔
149

150
    const searchpaths = await reduceAsync([], [...new Set(refAssetPaths)], async (res, file) => {
44✔
151
      const paths = await getAssetPaths(document, file, options, false);
88✔
152
      return [...new Set([...res, ...paths])];
88✔
153
    });
154

155
    const filtered = searchpaths.filter(
44✔
156
      (p) => isRemote(p) || p.includes(process.cwd()) || (base && p.includes(base)),
304✔
157
    );
158

159
    const inlineOptions = {
44✔
160
      assetPaths: [...filtered, ...assetPaths],
161
      maxFileSize: maxImageFileSize,
162
    };
163

164
    debug("Inline images:", inlineOptions, refAssets);
44✔
165

166
    postProcess.push(imageInliner(inlineOptions));
44✔
167
  }
168

169
  // Post-process critical css
170
  if (postProcess.length > 0) {
388✔
171
    criticalCSS = await postcss(postProcess)
76✔
172
      .process(criticalCSS, { from: undefined })
173
      .then((contents) => contents.css);
76✔
174
  }
175

176
  // Minify or prettify
177
  const cleanCSS = new CleanCSS(
388✔
178
    cleanCSSOptions || {
768✔
179
      level: {
180
        1: {
181
          all: true,
182
        },
183
        2: {
184
          all: false,
185
          removeDuplicateFontRules: true,
186
          removeDuplicateMediaBlocks: true,
187
          removeDuplicateRules: true,
188
          removeEmpty: true,
189
          mergeMedia: true,
190
        },
191
      },
192
    },
193
  );
194
  criticalCSS = cleanCSS.minify(criticalCSS).styles;
420✔
195

196
  const result = {
420✔
197
    css: criticalCSS,
198
  };
199

200
  // Define uncritical as lazy evaluated property
201
  const lazyUncritical = (orig, diff) =>
420✔
202
    function () {
388✔
203
      this._uncritical ||= removeDuplicateStyles(orig, diff);
88✔
204

205
      return this._uncritical;
88✔
206
    };
207

208
  Object.defineProperty(result, "uncritical", {
420✔
209
    get: lazyUncritical(document.css, criticalCSS),
210
  });
211

212
  // Inline
213
  if (inline) {
420✔
214
    const { replaceStylesheets } = inline;
96✔
215

216
    if (typeof replaceStylesheets === "function") {
96✔
217
      inline.replaceStylesheets = await replaceStylesheets(document, result.uncritical);
4✔
218
    }
219

220
    // If replaceStylesheets is not set via option and and uncritical is empty
221
    if (extract && replaceStylesheets === undefined && result.uncritical.trim() === "") {
96✔
222
      inline.replaceStylesheets = [];
4✔
223
    }
224

225
    if (target.uncritical) {
96✔
226
      const uncriticalHref = normalizePath(
4✔
227
        path.relative(document.cwd, path.resolve(base, target.uncritical)),
228
      );
229
      // Only replace stylesheets if the uncriticalHref is inside document.cwd and replaceStylesheets is not set via options
230
      if (!uncriticalHref.startsWith("../") && replaceStylesheets === undefined) {
4!
231
        inline.replaceStylesheets = [`/${uncriticalHref}`];
4✔
232
      }
233
    } else {
234
      inline.extract = extract;
92✔
235
    }
236

237
    const inlined = inlineCritical(document.contents.toString(), criticalCSS, {
96✔
238
      ...inline,
239
      basePath: document.cwd,
240
    });
241
    document.contents = Buffer.from(inlined);
96✔
242
  }
243

244
  // Clean tempfiles
245
  await document.cleanup();
388✔
246

247
  result.html = document.contents.toString();
388✔
248

249
  // Cleanup output
250
  return result;
388✔
251
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc