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

addyosmani / critical / 25988020510

17 May 2026 10:13AM UTC coverage: 90.131% (-0.1%) from 90.27%
25988020510

push

github

web-flow
chore: replace get-stdin with nodejs native stream/consumers (#616)

* chore: replace get-stdin with nodejs native streamConsumers

* chore: update lockfile after removing get-stdin

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Ben Zörb <ben@sommerlaune.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

535 of 613 branches covered (87.28%)

Branch coverage included in aggregate %.

0 of 1 new or added line in 1 file covered. (0.0%)

634 of 684 relevant lines covered (92.69%)

559.44 hits per line

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

76.15
/cli.js
1
#!/usr/bin/env node
2
import os from "node:os";
3
import process from "node:process";
4
import { text } from "node:stream/consumers";
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) {
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 {
NEW
256
  const data = process.stdin.isTTY ? "" : await text(process.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