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

addyosmani / critical / 25987114210

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

Pull #620

github

bezoerb
fix(ci): use setup-chrome action for reliable Chrome install

Instead of relying on Puppeteer's postinstall to download Chrome
(which fails intermittently on Windows CI runners), use the
browser-actions/setup-chrome action to install a stable Chrome
and point Puppeteer to it via PUPPETEER_EXECUTABLE_PATH.
PUPPETEER_SKIP_DOWNLOAD=true prevents redundant Chrome downloads.
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

93.26
/src/config.js
1
import process from "node:process";
2
import Joi from "joi";
3
import debugBase from "debug";
4
import { traverse, STOP } from "async-traverse-tree";
5
import { ConfigError } from "./errors.js";
6

7
const debug = debugBase("critical:config");
40✔
8

9
export const DEFAULT = {
40✔
10
  width: 1300,
11
  height: 900,
12
  timeout: 30_000,
13
  maxImageFileSize: 10_240,
14
  inline: false,
15
  strict: false,
16
  extract: false,
17
  inlineImages: false,
18
  ignoreInlinedStyles: false,
19
  concurrency: Number.POSITIVE_INFINITY,
20
  include: [],
21
};
22

23
const schema = Joi.object()
40✔
24
  .keys({
25
    html: Joi.string(),
26
    src: [Joi.string(), Joi.object()],
27
    css: [Joi.string(), Joi.array()],
28
    base: Joi.string(),
29
    strict: Joi.boolean().default(DEFAULT.strict),
30
    ignoreInlinedStyles: Joi.boolean().default(DEFAULT.ignoreInlinedStyles),
31
    extract: Joi.boolean().default(DEFAULT.extract),
32
    inlineImages: Joi.boolean().default(DEFAULT.inlineImages),
33
    postcss: Joi.array(),
34
    ignore: [Joi.array(), Joi.object().unknown(true)],
35
    width: Joi.number().default(DEFAULT.width),
36
    height: Joi.number().default(DEFAULT.height),
37
    dimensions: Joi.array().items({ width: Joi.number(), height: Joi.number() }),
38
    inline: [Joi.boolean().default(DEFAULT.inline), Joi.object().unknown(true)],
39
    maxImageFileSize: Joi.number().default(DEFAULT.maxImageFileSize),
40
    include: Joi.any().default(DEFAULT.include),
41
    concurrency: Joi.number().default(DEFAULT.concurrency),
42
    user: Joi.string(),
43
    pass: Joi.string(),
44
    request: Joi.object().unknown(true),
45
    penthouse: Joi.object()
46
      .keys({
47
        url: Joi.any().forbidden(),
48
        css: Joi.any().forbidden(),
49
        width: Joi.any().forbidden(),
50
        height: Joi.any().forbidden(),
51
        timeout: Joi.number().default(DEFAULT.timeout),
52
        forceInclude: Joi.any(),
53
        maxEmbeddedBase64Length: Joi.number(),
54
      })
55
      .unknown(true),
56
    rebase: [
57
      Joi.object().keys({
58
        from: Joi.string(),
59
        to: Joi.string(),
60
      }),
61
      Joi.func(),
62
      Joi.boolean(),
63
    ],
64
    target: [
65
      Joi.string(),
66
      Joi.object().keys({
67
        css: Joi.string(),
68
        html: Joi.string(),
69
        uncritical: Joi.string(),
70
      }),
71
    ],
72
    assetPaths: Joi.array().items(Joi.string()),
73
    userAgent: Joi.string(),
74
    cleanCSS: Joi.object().unknown(true),
75
  })
76
  .label("options")
77
  .xor("html", "src");
78

79
export async function getOptions(options = {}) {
460✔
80
  const parsedOptions = await traverse(options, (key, value) => {
460✔
81
    if (["css", "html", "src"].includes(key)) {
3,324✔
82
      return STOP;
580✔
83
    }
84

85
    if (typeof value === "string") {
2,744✔
86
      try {
748✔
87
        return JSON.parse(value);
748✔
88
      } catch {}
89
    }
90

91
    return value;
2,740✔
92
  });
93

94
  const { error, value } = schema.validate(parsedOptions);
460✔
95

96
  const { inline, dimensions, penthouse = {}, target, ignore } = value || {};
460!
97

98
  if (error) {
460✔
99
    const { details = [] } = error;
28✔
100
    const [detail = {}] = details;
28✔
101
    const { message = "invalid options" } = detail;
28✔
102

103
    throw new ConfigError(message);
28✔
104
  }
105

106
  if (!dimensions || dimensions.length === 0) {
432✔
107
    value.dimensions = [
376✔
108
      {
109
        width: options.width || DEFAULT.width,
544✔
110
        height: options.height || DEFAULT.height,
544✔
111
      },
112
    ];
113
  }
114

115
  if (typeof target === "string") {
432✔
116
    const key = target.endsWith(".css") ? "css" : "html";
304✔
117
    value.target = { [key]: target };
304✔
118
  }
119

120
  // Set inline options
121
  value.inline = Boolean(inline) && {
432✔
122
    basePath: value.base || process.cwd(),
108!
123
    ...(inline === true ? { strategy: "media" } : inline),
108✔
124
  };
125

126
  if (
460✔
127
    value.inline.replaceStylesheets !== undefined &&
464✔
128
    !Array.isArray(value.inline.replaceStylesheets)
129
  ) {
130
    if (value.inline.replaceStylesheets === "false") {
4!
UNCOV
131
      value.inline.replaceStylesheets = false;
×
132
    } else if (typeof value.inline.replaceStylesheets !== "function") {
4!
133
      value.inline.replaceStylesheets = [value.inline.replaceStylesheets];
×
134
    }
135
  }
136

137
  // Set penthouse options
138
  value.penthouse = {
432✔
139
    forceInclude: value.include,
140
    timeout: DEFAULT.timeout,
141
    maxEmbeddedBase64Length: value.maxImageFileSize,
142
    ...penthouse,
143
  };
144

145
  if (ignore && Array.isArray(ignore)) {
432✔
146
    value.ignore = {
36✔
147
      atrule: ignore,
148
      rule: ignore,
149
      decl: ignore,
150
    };
151
  }
152

153
  if (target && target.uncritical) {
432✔
154
    value.extract = true;
16✔
155
  }
156

157
  debug(value);
432✔
158

159
  return value;
432✔
160
}
161

162
export const validate = (key, val) => {
40✔
163
  const { error } = schema.validate({ [key]: val, html: "<html/>" });
124✔
164
  if (error) {
124✔
165
    return false;
28✔
166
  }
167

168
  return true;
96✔
169
};
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