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

addyosmani / critical / 25985035616

17 May 2026 07:47AM UTC coverage: 90.27% (+1.6%) from 88.691%
25985035616

Pull #620

github

bezoerb
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.
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%)

419.47 hits per line

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

77.34
/cli.js
1
#!/usr/bin/env node
2
import os from "node:os";
3
import process from "node:process";
4
import stdin from "get-stdin";
5
import groupArgs from "group-args";
6
import indentString from "indent-string";
7
import { escapeRegExp, isObject, isString, reduce } from "lodash-es";
8
import meow from "meow";
9
import pico from "picocolors";
10
import { validate } from "./src/config.js";
11
import { generate } from "./index.js";
12

13
const help = `
21✔
14
Usage: critical <input> [<option>]
15

16
Options:
17
  -b, --base              Your base directory
18
  -c, --css               Your CSS Files (optional)
19
  -w, --width             Viewport width
20
  -h, --height            Viewport height
21
  -i, --inline            Generate the HTML with inlined critical-path CSS
22
  -e, --extract           Extract inlined styles from referenced stylesheets
23

24
  --inlineImages          Inline images
25
  --dimensions            Pass dimensions e.g. 1300x900
26
  --ignore                RegExp, @type or selector to ignore
27
  --ignore-[OPTION]       Pass options to postcss-discard. See https://goo.gl/HGo5YV
28
  --ignoreInlinedStyles   Ignore inlined stylesheets
29
  --include               RegExp, @type or selector to include
30
  --include-[OPTION]      Pass options to inline-critical. See https://goo.gl/w6SHJM
31
  --assetPaths            Directories/Urls where the inliner should start looking for assets
32
  --user                  RFC2617 basic authorization user
33
  --pass                  RFC2617 basic authorization password
34
  --penthouse-[OPTION]    Pass options to penthouse. See https://goo.gl/PQ5HLL
35
  --ua, --userAgent       User agent to use when fetching remote src
36
  --strict                Throw an error on css parsing errors or if no css is found
37
`;
38

39
const meowOpts = {
21✔
40
  importMeta: import.meta,
41
  flags: {
42
    base: {
43
      type: "string",
44
      shortFlag: "b",
45
    },
46
    css: {
47
      type: "string",
48
      shortFlag: "c",
49
      isMultiple: true,
50
    },
51
    width: {
52
      type: "number",
53
      shortFlag: "w",
54
    },
55
    height: {
56
      type: "number",
57
      shortFlag: "h",
58
    },
59
    inline: {
60
      type: "boolean",
61
      shortFlag: "i",
62
    },
63
    extract: {
64
      type: "boolean",
65
      shortFlag: "e",
66
      default: false,
67
    },
68
    inlineImages: {
69
      type: "boolean",
70
    },
71
    ignoreInlinedStyles: {
72
      type: "boolean",
73
      default: false,
74
    },
75
    ignore: {
76
      type: "string",
77
    },
78
    user: {
79
      type: "string",
80
    },
81
    strict: {
82
      type: "boolean",
83
      default: false,
84
    },
85
    pass: {
86
      type: "string",
87
    },
88
    userAgent: {
89
      type: "string",
90
      shortFlag: "ua",
91
    },
92
    dimensions: {
93
      type: "string",
94
      isMultiple: true,
95
    },
96
  },
97
};
98

99
const cli = meow(help, meowOpts);
21✔
100

101
const groupKeys = ["ignore", "inline", "penthouse", "target", "request"];
21✔
102
// Group args for inline-critical and penthouse
103
const grouped = {
21✔
104
  ...cli.flags,
105
  ...groupArgs(
106
    groupKeys,
107
    {
108
      delimiter: "-",
109
    },
110
    meowOpts,
111
  ),
112
};
113

114
/**
115
 * Check if key is an alias
116
 * @param {string} key Key to check
117
 * @returns {boolean} True for alias
118
 */
119
const isAlias = (key) => {
21✔
120
  if (isString(key) && key.length > 1) {
216✔
121
    return false;
174✔
122
  }
123

124
  const aliases = Object.keys(meowOpts.flags)
42✔
125
    .filter((k) => meowOpts.flags[k].shortFlag)
588✔
126
    .map((k) => meowOpts.flags[k].shortFlag);
294✔
127

128
  return aliases.includes(key);
42✔
129
};
130

131
/**
132
 * Check if value is an empty object
133
 * @param {mixed} val Value to check
134
 * @returns {boolean} Whether or not this is an empty object
135
 */
136
const isEmptyObj = (val) => isObject(val) && Object.keys(val).length === 0;
36✔
137

138
/**
139
 * Check if value is transformed to {default: val}
140
 * @param {mixed} val Value to check
141
 * @returns {boolean} True if it's been converted to {default: value}
142
 */
143
const isGroupArgsDefault = (val) => isObject(val) && Object.keys(val).length === 1 && val.default;
33✔
144

145
/**
146
 * Return regex if value is a string like this: '/.../g'
147
 * @param {mixed} val Value to process
148
 * @returns {mixed} Mapped values
149
 */
150
const mapRegExpStr = (val) => {
21✔
151
  if (isString(val)) {
312✔
152
    const { groups } = val.match(/^\/(?<regex>[^/]+)(?:\/?(?<flags>[igmy]+))?\/$/) || {};
129✔
153
    const { regex, flags } = groups || {};
129✔
154

155
    return (groups && new RegExp(escapeRegExp(regex), flags)) || val;
129✔
156
  }
157

158
  if (Array.isArray(val)) {
183✔
159
    return val.map((v) => mapRegExpStr(v));
117✔
160
  }
161

162
  return val;
120✔
163
};
164

165
const normalizedFlags = reduce(
21✔
166
  grouped,
167
  (res, val, key) => {
168
    // Cleanup groupArgs mess ;)
169
    if (groupKeys.includes(key)) {
237✔
170
      // An empty object means param without value, just true
171
      if (isEmptyObj(val)) {
36✔
172
        val = true;
3✔
173
      } else if (isGroupArgsDefault(val)) {
33✔
174
        val = val.default;
9✔
175
      }
176
    }
177

178
    // Cleanup camelized group keys
179
    if (groupKeys.some((k) => key.includes(k)) && !validate(key, val)) {
933✔
180
      return res;
21✔
181
    }
182

183
    if (!isAlias(key)) {
216✔
184
      res[key] = mapRegExpStr(val);
195✔
185
    }
186

187
    return res;
216✔
188
  },
189
  {},
190
);
191

192
function showError(err) {
NEW
193
  process.stderr.write(indentString(pico.red("Error: ") + err.message || err, 3));
×
194
  process.stderr.write(os.EOL);
×
195
  process.stderr.write(indentString(help, 3));
×
196
  process.exit(1);
×
197
}
198

199
function run(data) {
200
  const { _: inputs = [], css, ...opts } = { ...normalizedFlags };
21✔
201

202
  // Detect css globbing
203
  const cssBegin = process.argv.findIndex((el) => ["--css", "-c"].includes(el));
102✔
204
  const cssEnd = process.argv.findIndex((el, index) => index > cssBegin && el.startsWith("-"));
180✔
205
  const cssCheck =
206
    cssBegin >= 0 ? process.argv.slice(cssBegin, cssEnd > 0 ? cssEnd : undefined) : [];
21!
207
  const additionalCss = inputs.filter((file) => cssCheck.includes(file));
93✔
208
  // Just take the first html input as we don't support multiple html sources for
209
  const [input] = inputs.filter((file) => !additionalCss.includes(file)); // eslint-disable-line unicorn/prefer-array-find
93✔
210

211
  if (Array.isArray(opts.dimensions)) {
21!
212
    opts.dimensions = opts.dimensions.reduce(
21✔
213
      (result, data) => [
9✔
214
        ...result,
215
        ...data.split(",").map((dimension) => {
216
          const [width, height] = dimension.split("x");
15✔
217
          return { width: Number.parseInt(width, 10), height: Number.parseInt(height, 10) };
15✔
218
        }),
219
      ],
220
      [],
221
    );
222
  }
223

224
  if (Array.isArray(css)) {
21✔
225
    opts.css = [...css, ...additionalCss].filter(Boolean);
18✔
226
  } else if (css || additionalCss.length > 0) {
3!
227
    opts.css = [css, ...additionalCss].filter(Boolean);
3✔
228
  }
229

230
  if (data) {
21!
231
    opts.html = data;
×
232
  } else {
233
    opts.src = input;
21✔
234
  }
235

236
  try {
21✔
237
    generate(opts, (error, val) => {
21✔
238
      if (error) {
×
239
        showError(error);
×
240
      } else if (opts.inline) {
×
241
        process.stdout.write(val.html, process.exit);
×
242
      } else if (opts.extract) {
×
243
        process.stdout.write(val.uncritical, process.exit);
×
244
      } else {
245
        process.stdout.write(val.css, process.exit);
×
246
      }
247
    });
248
  } catch (error) {
249
    showError(error);
×
250
  }
251
}
252

253
if (cli.input[0]) {
21!
254
  run();
21✔
255
} else {
256
  const data = await stdin();
×
257
  run(data);
×
258
}
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