• 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

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 = `
28✔
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 = {
28✔
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);
28✔
100

101
const groupKeys = ["ignore", "inline", "penthouse", "target", "request"];
28✔
102
// Group args for inline-critical and penthouse
103
const grouped = {
28✔
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) => {
28✔
120
  if (isString(key) && key.length > 1) {
288✔
121
    return false;
232✔
122
  }
123

124
  const aliases = Object.keys(meowOpts.flags)
56✔
125
    .filter((k) => meowOpts.flags[k].shortFlag)
784✔
126
    .map((k) => meowOpts.flags[k].shortFlag);
392✔
127

128
  return aliases.includes(key);
56✔
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;
48✔
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;
44✔
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) => {
28✔
151
  if (isString(val)) {
416✔
152
    const { groups } = val.match(/^\/(?<regex>[^/]+)(?:\/?(?<flags>[igmy]+))?\/$/) || {};
172✔
153
    const { regex, flags } = groups || {};
172✔
154

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

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

162
  return val;
160✔
163
};
164

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

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

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

187
    return res;
288✔
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 };
28✔
201

202
  // Detect css globbing
203
  const cssBegin = process.argv.findIndex((el) => ["--css", "-c"].includes(el));
136✔
204
  const cssEnd = process.argv.findIndex((el, index) => index > cssBegin && el.startsWith("-"));
240✔
205
  const cssCheck =
206
    cssBegin >= 0 ? process.argv.slice(cssBegin, cssEnd > 0 ? cssEnd : undefined) : [];
28!
207
  const additionalCss = inputs.filter((file) => cssCheck.includes(file));
124✔
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
124✔
210

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

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

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

236
  try {
28✔
237
    generate(opts, (error, val) => {
28✔
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]) {
28!
254
  run();
28✔
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