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

addyosmani / critical / 25987475649

17 May 2026 09:46AM UTC coverage: 90.27% (+1.6%) from 88.691%
25987475649

push

github

web-flow
vite+ & dependency update (#620)

* chore: switch from npm to pnpm

* chore: bump dependencies to latest compatible versions

* chore: update cross-env, nock, normalize-newline to latest

* chore: update get-stdin, find-up, data-uri-to-buffer to latest

* chore: update serve-static and finalhandler to v2

* chore: update globby to v16

* chore: update meow to v14

* chore: update through2 to v5

* chore: update joi to v18

* chore: update got to v14

* chore: migrate toolchain to Vite+

- Replace Jest with Vitest (via vite-plus)
- Replace XO/ESLint with Oxlint
- Replace Prettier with Oxfmt
- Convert all done() callback tests to Promise-based
- Add await to all expect().rejects/resolves assertions
- Update CI workflow to use voidzero-dev/setup-vp@v1
- Add coverage reporting with v8 provider
- Add vite.config.ts with test, lint, and fmt config

* chore: upgrade got v14 -> v15, drop Node.js 20

BREAKING CHANGE: Minimum Node.js version is now 22.12.0

* fix: patch minimatch ReDoS vulnerability via pnpm override

Override minimatch to >=3.1.4 in pnpm-workspace.yaml to resolve
3 high-severity ReDoS vulnerabilities (GHSA-3ppc-4f35-3m26,
GHSA-7r86-cg39-jmmj, GHSA-23c5-xmqv-rm74) in postcss-url's
transitive dependency.

* fix(ci): require Node.js >=22.13 for pnpm 11 compatibility

- Update engines to >=22.13.0 (pnpm@11.1.2 requires it)
- Use node version '22' in CI matrix (latest 22.x)
- Set run-install: false in setup-vp to use explicit install step
- Add github-token to coveralls action

* fix(ci): add @vitest/coverage-v8 and Node 24 to matrix

- Add missing @vitest/coverage-v8 devDependency for coverage reports
- Add Node.js 24 to CI test matrix

* fix: prefix unused parameters with underscore

* fix: use threads pool with forks override for CLI tests

Worker forks crash on Windows. Switch default pool to threads and use
Vitest projects config to run cli.test.js (which needs process.chdir)
in the forks pool.

* fix(ci): explicitly install Puppeteer Chrome b... (continued)

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.45 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