@projectwallace/css-code-coverage 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2,7 +2,6 @@
2
2
  import { parseArgs, styleText } from "node:util";
3
3
  import * as v from "valibot";
4
4
  import { format } from "@projectwallace/format-css";
5
- import { tokenTypes, tokenize } from "css-tree/tokenizer";
6
5
  import { readFile, readdir, stat } from "node:fs/promises";
7
6
  import { join } from "node:path";
8
7
 
@@ -90,69 +89,131 @@ function parse_coverage(input) {
90
89
  }
91
90
 
92
91
  //#endregion
93
- //#region src/lib/prettify.ts
94
- let irrelevant_tokens = new Set([
95
- tokenTypes.EOF,
96
- tokenTypes.BadString,
97
- tokenTypes.BadUrl,
98
- tokenTypes.WhiteSpace,
99
- tokenTypes.Semicolon,
100
- tokenTypes.Comment,
101
- tokenTypes.Colon
102
- ]);
103
- function prettify(coverage) {
104
- return coverage.map(({ url, text, ranges }) => {
105
- let formatted = format(text);
106
- let ext_ranges = ranges.map(({ start, end }) => ({
107
- start,
108
- end,
109
- tokens: []
110
- }));
111
- function is_in_range(start, end) {
112
- let range_index = 0;
113
- for (let range of ext_ranges) {
114
- if (range.start > end) return -1;
115
- if (range.start <= start && range.end >= end) return range_index;
116
- range_index++;
92
+ //#region src/lib/chunkify.ts
93
+ function merge(stylesheet) {
94
+ let new_chunks = [];
95
+ let previous_chunk;
96
+ for (let i = 0; i < stylesheet.chunks.length; i++) {
97
+ let chunk = stylesheet.chunks.at(i);
98
+ if (/^\s+$/.test(stylesheet.text.slice(chunk.start_offset, chunk.end_offset))) continue;
99
+ let latest_chunk = new_chunks.at(-1);
100
+ if (i > 0 && previous_chunk && latest_chunk) {
101
+ if (previous_chunk.is_covered === chunk.is_covered) {
102
+ latest_chunk.end_offset = chunk.end_offset;
103
+ previous_chunk = chunk;
104
+ continue;
105
+ } else if (/^\s+$/.test(stylesheet.text.slice(chunk.start_offset, chunk.end_offset)) || chunk.end_offset === chunk.start_offset) {
106
+ latest_chunk.end_offset = chunk.end_offset;
107
+ continue;
117
108
  }
118
- return -1;
119
109
  }
120
- let index = 0;
121
- tokenize(text, (type, start, end) => {
122
- if (irrelevant_tokens.has(type)) return;
123
- index++;
124
- let range_index = is_in_range(start, end);
125
- if (range_index !== -1) ext_ranges[range_index].tokens.push(index);
126
- });
127
- let new_tokens = /* @__PURE__ */ new Map();
128
- index = 0;
129
- tokenize(formatted, (type, start, end) => {
130
- if (irrelevant_tokens.has(type)) return;
131
- index++;
132
- new_tokens.set(index, {
133
- start,
134
- end
110
+ previous_chunk = chunk;
111
+ new_chunks.push(chunk);
112
+ }
113
+ return {
114
+ ...stylesheet,
115
+ chunks: new_chunks
116
+ };
117
+ }
118
+ function chunkify(stylesheet) {
119
+ let chunks = [];
120
+ let offset = 0;
121
+ for (let range of stylesheet.ranges) {
122
+ if (offset !== range.start) {
123
+ chunks.push({
124
+ start_offset: offset,
125
+ end_offset: range.start,
126
+ is_covered: false
135
127
  });
128
+ offset = range.start;
129
+ }
130
+ chunks.push({
131
+ start_offset: range.start,
132
+ end_offset: range.end,
133
+ is_covered: true
136
134
  });
137
- let new_ranges = [];
138
- for (let range of ext_ranges) {
139
- let start_token = new_tokens.get(range.tokens.at(0));
140
- let end_token = new_tokens.get(range.tokens.at(-1));
141
- if (start_token !== void 0 && end_token !== void 0) new_ranges.push({
142
- start: start_token.start,
143
- end: end_token.end
144
- });
135
+ offset = range.end;
136
+ }
137
+ if (offset !== stylesheet.text.length - 1) chunks.push({
138
+ start_offset: offset,
139
+ end_offset: stylesheet.text.length,
140
+ is_covered: false
141
+ });
142
+ return merge({
143
+ url: stylesheet.url,
144
+ text: stylesheet.text,
145
+ chunks
146
+ });
147
+ }
148
+
149
+ //#endregion
150
+ //#region src/lib/prettify.ts
151
+ function prettify(stylesheet) {
152
+ let line = 1;
153
+ let offset = 0;
154
+ let pretty_chunks = stylesheet.chunks.map((chunk, index) => {
155
+ let css = format(stylesheet.text.slice(chunk.start_offset, chunk.end_offset - 1)).trim();
156
+ if (chunk.is_covered) {
157
+ let is_last = index === stylesheet.chunks.length - 1;
158
+ if (index === 0) css = css + (is_last ? "" : "\n");
159
+ else if (index === stylesheet.chunks.length - 1) css = "\n" + css;
160
+ else css = "\n" + css + "\n";
145
161
  }
162
+ let line_count = css.split("\n").length;
163
+ let start_offset = offset;
164
+ let end_offset = offset + css.length - 1;
165
+ let start_line = line;
166
+ let end_line = line + line_count;
167
+ line = end_line;
168
+ offset = end_offset;
146
169
  return {
147
- url,
148
- text: formatted,
149
- ranges: new_ranges
170
+ ...chunk,
171
+ start_offset,
172
+ start_line,
173
+ end_line: end_line - 1,
174
+ end_offset,
175
+ css,
176
+ total_lines: end_line - start_line
150
177
  };
151
178
  });
179
+ return {
180
+ ...stylesheet,
181
+ chunks: pretty_chunks,
182
+ text: pretty_chunks.map(({ css }) => css).join("\n")
183
+ };
152
184
  }
153
185
 
154
186
  //#endregion
155
187
  //#region src/lib/decuplicate.ts
188
+ function concatenate(ranges) {
189
+ let result = [];
190
+ for (let range of ranges) if (result.length > 0 && (result.at(-1).end === range.start - 1 || result.at(-1).end === range.start)) result.at(-1).end = range.end;
191
+ else result.push(range);
192
+ return result;
193
+ }
194
+ function dedupe_list(ranges) {
195
+ let new_ranges = /* @__PURE__ */ new Set();
196
+ outer: for (let range of ranges) {
197
+ for (let processed_range of new_ranges) {
198
+ if (range.start <= processed_range.start && range.end >= processed_range.end) {
199
+ new_ranges.delete(processed_range);
200
+ new_ranges.add(range);
201
+ continue outer;
202
+ }
203
+ if (range.start >= processed_range.start && range.end <= processed_range.end) continue outer;
204
+ if (range.start < processed_range.end && range.start > processed_range.start && range.end > processed_range.end) {
205
+ new_ranges.delete(processed_range);
206
+ new_ranges.add({
207
+ start: processed_range.start,
208
+ end: range.end
209
+ });
210
+ continue outer;
211
+ }
212
+ }
213
+ new_ranges.add(range);
214
+ }
215
+ return new_ranges;
216
+ }
156
217
  /**
157
218
  * @description
158
219
  * prerequisites
@@ -182,7 +243,7 @@ function deduplicate_entries(entries) {
182
243
  return Array.from(checked_stylesheets, ([text, { url, ranges }]) => ({
183
244
  text,
184
245
  url,
185
- ranges
246
+ ranges: concatenate(dedupe_list(ranges.sort((a, b) => a.start - b.start))).sort((a, b) => a.start - b.start)
186
247
  }));
187
248
  }
188
249
 
@@ -198,17 +259,43 @@ function ext(url) {
198
259
  }
199
260
  }
200
261
 
262
+ //#endregion
263
+ //#region src/lib/html-parser.ts
264
+ /**
265
+ * @description
266
+ * Very, very naive but effective DOMParser.
267
+ * It can only find <style> elements and their .textContent
268
+ */
269
+ var DOMParser = class {
270
+ parseFromString(html, _type) {
271
+ let styles = [];
272
+ let lower = html.toLowerCase();
273
+ let pos = 0;
274
+ while (true) {
275
+ let open = lower.indexOf("<style", pos);
276
+ if (open === -1) break;
277
+ let start = lower.indexOf(">", open);
278
+ if (start === -1) break;
279
+ let close = lower.indexOf("</style>", start);
280
+ if (close === -1) break;
281
+ let text = html.slice(start + 1, close);
282
+ styles.push({ textContent: text });
283
+ pos = close + 8;
284
+ }
285
+ return { querySelectorAll(selector) {
286
+ return styles;
287
+ } };
288
+ }
289
+ };
290
+
201
291
  //#endregion
202
292
  //#region src/lib/remap-html.ts
203
- async function get_dom_parser() {
204
- if (typeof window !== "undefined" && "DOMParser" in window)
205
- /* v8 ignore */
206
- return new window.DOMParser();
207
- let { DOMParser } = await import("./esm--VCpEgdH.js");
293
+ function get_dom_parser() {
294
+ if (typeof window !== "undefined" && "DOMParser" in window) return new window.DOMParser();
208
295
  return new DOMParser();
209
296
  }
210
- async function remap_html(html, old_ranges) {
211
- let doc = (await get_dom_parser()).parseFromString(html, "text/html");
297
+ function remap_html(html, old_ranges) {
298
+ let doc = get_dom_parser().parseFromString(html, "text/html");
212
299
  let combined_css = "";
213
300
  let new_ranges = [];
214
301
  let current_offset = 0;
@@ -236,7 +323,20 @@ async function remap_html(html, old_ranges) {
236
323
  function is_html(text) {
237
324
  return /<\/?(html|body|head|div|span|script|style)/i.test(text);
238
325
  }
239
- async function filter_coverage(coverage) {
326
+ const SELECTOR_REGEX = /(@[a-z-]+|\[[^\]]+\]|[a-zA-Z_#.-][a-zA-Z0-9_-]*)\s*\{/;
327
+ const DECLARATION_REGEX = /^\s*[a-zA-Z-]+\s*:\s*.+;?\s*$/m;
328
+ function is_css_like(text) {
329
+ return SELECTOR_REGEX.test(text) || DECLARATION_REGEX.test(text);
330
+ }
331
+ function is_js_like(text) {
332
+ try {
333
+ new Function(text);
334
+ return true;
335
+ } catch {
336
+ return false;
337
+ }
338
+ }
339
+ function filter_coverage(coverage) {
240
340
  let result = [];
241
341
  for (let entry of coverage) {
242
342
  let extension = ext(entry.url).toLowerCase();
@@ -246,7 +346,7 @@ async function filter_coverage(coverage) {
246
346
  continue;
247
347
  }
248
348
  if (is_html(entry.text)) {
249
- let { css, ranges } = await remap_html(entry.text, entry.ranges);
349
+ let { css, ranges } = remap_html(entry.text, entry.ranges);
250
350
  result.push({
251
351
  url: entry.url,
252
352
  text: css,
@@ -254,7 +354,7 @@ async function filter_coverage(coverage) {
254
354
  });
255
355
  continue;
256
356
  }
257
- result.push({
357
+ if (is_css_like(entry.text) && !is_js_like(entry.text)) result.push({
258
358
  url: entry.url,
259
359
  text: entry.text,
260
360
  ranges: entry.ranges
@@ -263,12 +363,85 @@ async function filter_coverage(coverage) {
263
363
  return result;
264
364
  }
265
365
 
366
+ //#endregion
367
+ //#region src/lib/extend-ranges.ts
368
+ const AT_SIGN = 64;
369
+ const LONGEST_ATRULE_NAME = 28;
370
+ function extend_ranges(coverage) {
371
+ return coverage.map(({ text, ranges, url }) => {
372
+ return {
373
+ text,
374
+ ranges: ranges.map((range, index) => {
375
+ let prev_range = ranges[index - 1];
376
+ for (let i = range.start; i >= range.start - LONGEST_ATRULE_NAME; i--) {
377
+ if (prev_range && prev_range.end > i) break;
378
+ let char_position = i;
379
+ if (text.charCodeAt(char_position) === AT_SIGN) {
380
+ range.start = char_position;
381
+ let next_offset = range.end;
382
+ let next_char$1 = text.charAt(next_offset);
383
+ while (/\s/.test(next_char$1)) {
384
+ next_offset++;
385
+ next_char$1 = text.charAt(next_offset);
386
+ }
387
+ if (next_char$1 === "{") range.end = range.end + 1;
388
+ break;
389
+ }
390
+ }
391
+ let offset = range.end;
392
+ let next_char = text.charAt(offset);
393
+ while (/\s/.test(next_char)) {
394
+ offset++;
395
+ next_char = text.charAt(offset);
396
+ }
397
+ if (next_char === "}") range.end = offset + 1;
398
+ return range;
399
+ }),
400
+ url
401
+ };
402
+ });
403
+ }
404
+
266
405
  //#endregion
267
406
  //#region src/lib/index.ts
268
407
  function ratio(fraction, total) {
269
408
  if (total === 0) return 0;
270
409
  return fraction / total;
271
410
  }
411
+ function calculate_stylesheet_coverage({ text, url, chunks }) {
412
+ let uncovered_bytes = 0;
413
+ let covered_bytes = 0;
414
+ let total_bytes = 0;
415
+ let total_lines = 0;
416
+ let covered_lines = 0;
417
+ let uncovered_lines = 0;
418
+ for (let chunk of chunks) {
419
+ let lines = chunk.total_lines;
420
+ let bytes = chunk.end_offset - chunk.start_offset;
421
+ total_lines += lines;
422
+ total_bytes += bytes;
423
+ if (chunk.is_covered) {
424
+ covered_lines += lines;
425
+ covered_bytes += bytes;
426
+ } else {
427
+ uncovered_lines += lines;
428
+ uncovered_bytes += bytes;
429
+ }
430
+ }
431
+ return {
432
+ url,
433
+ text,
434
+ uncovered_bytes,
435
+ covered_bytes,
436
+ total_bytes,
437
+ line_coverage_ratio: ratio(covered_lines, total_lines),
438
+ byte_coverage_ratio: ratio(covered_bytes, total_bytes),
439
+ total_lines,
440
+ covered_lines,
441
+ uncovered_lines,
442
+ chunks
443
+ };
444
+ }
272
445
  /**
273
446
  * @description
274
447
  * CSS Code Coverage calculation
@@ -280,93 +453,16 @@ function ratio(fraction, total) {
280
453
  * 4. Calculate used/unused CSS bytes (fastest path, no inspection of the actual CSS needed)
281
454
  * 5. Calculate line-coverage, byte-coverage per stylesheet
282
455
  */
283
- async function calculate_coverage(coverage) {
456
+ function calculate_coverage(coverage) {
284
457
  let total_files_found = coverage.length;
285
- let coverage_per_stylesheet = deduplicate_entries(prettify(await filter_coverage(coverage))).map(({ text, url, ranges }) => {
286
- function is_line_covered(line, start_offset) {
287
- let end = start_offset + line.length;
288
- let next_offset = end + 1;
289
- let is_empty = /^\s*$/.test(line);
290
- let is_closing_brace = line.endsWith("}");
291
- if (!is_empty && !is_closing_brace) for (let range of ranges) {
292
- if (range.start > end || range.end < start_offset) continue;
293
- if (range.start <= start_offset && range.end >= end) return true;
294
- else if (line.startsWith("@") && range.start > start_offset && range.start < next_offset) return true;
295
- }
296
- return false;
297
- }
298
- let lines = text.split("\n");
299
- let total_file_lines = lines.length;
300
- let line_coverage = new Uint8Array(total_file_lines);
301
- let file_lines_covered = 0;
302
- let file_total_bytes = text.length;
303
- let file_bytes_covered = 0;
304
- let offset = 0;
305
- for (let index = 0; index < lines.length; index++) {
306
- let line = lines[index];
307
- let start = offset;
308
- let next_offset = offset + line.length + 1;
309
- let is_empty = /^\s*$/.test(line);
310
- let is_closing_brace = line.endsWith("}");
311
- let is_in_range = is_line_covered(line, start);
312
- let is_covered = false;
313
- let prev_is_covered = index > 0 && line_coverage[index - 1] === 1;
314
- if (is_in_range && !is_closing_brace && !is_empty) is_covered = true;
315
- else if ((is_empty || is_closing_brace) && prev_is_covered) is_covered = true;
316
- else if (is_empty && !prev_is_covered && is_line_covered(lines[index + 1], next_offset)) is_covered = true;
317
- line_coverage[index] = is_covered ? 1 : 0;
318
- if (is_covered) {
319
- file_lines_covered++;
320
- file_bytes_covered += line.length + 1;
321
- }
322
- offset = next_offset;
323
- }
324
- let chunks = [{
325
- start_line: 1,
326
- is_covered: line_coverage[0] === 1,
327
- end_line: 1,
328
- total_lines: 1
329
- }];
330
- for (let index = 1; index < line_coverage.length; index++) {
331
- let is_covered = line_coverage.at(index);
332
- if (is_covered !== line_coverage.at(index - 1)) {
333
- let last_chunk$1 = chunks.at(-1);
334
- last_chunk$1.end_line = index;
335
- last_chunk$1.total_lines = index - last_chunk$1.start_line + 1;
336
- chunks.push({
337
- start_line: index + 1,
338
- is_covered: is_covered === 1,
339
- end_line: index,
340
- total_lines: 0
341
- });
342
- }
343
- }
344
- let last_chunk = chunks.at(-1);
345
- last_chunk.total_lines = line_coverage.length + 1 - last_chunk.start_line;
346
- last_chunk.end_line = line_coverage.length;
347
- return {
348
- url,
349
- text,
350
- ranges,
351
- unused_bytes: file_total_bytes - file_bytes_covered,
352
- used_bytes: file_bytes_covered,
353
- total_bytes: file_total_bytes,
354
- line_coverage_ratio: ratio(file_lines_covered, total_file_lines),
355
- byte_coverage_ratio: ratio(file_bytes_covered, file_total_bytes),
356
- line_coverage,
357
- total_lines: total_file_lines,
358
- covered_lines: file_lines_covered,
359
- uncovered_lines: total_file_lines - file_lines_covered,
360
- chunks
361
- };
362
- });
458
+ let coverage_per_stylesheet = extend_ranges(deduplicate_entries(filter_coverage(coverage))).map((sheet) => chunkify(sheet)).map((sheet) => prettify(sheet)).map((stylesheet) => calculate_stylesheet_coverage(stylesheet));
363
459
  let { total_lines, total_covered_lines, total_uncovered_lines, total_bytes, total_used_bytes, total_unused_bytes } = coverage_per_stylesheet.reduce((totals, sheet) => {
364
460
  totals.total_lines += sheet.total_lines;
365
461
  totals.total_covered_lines += sheet.covered_lines;
366
462
  totals.total_uncovered_lines += sheet.uncovered_lines;
367
463
  totals.total_bytes += sheet.total_bytes;
368
- totals.total_used_bytes += sheet.used_bytes;
369
- totals.total_unused_bytes += sheet.unused_bytes;
464
+ totals.total_used_bytes += sheet.covered_bytes;
465
+ totals.total_unused_bytes += sheet.uncovered_bytes;
370
466
  return totals;
371
467
  }, {
372
468
  total_lines: 0,
@@ -380,9 +476,9 @@ async function calculate_coverage(coverage) {
380
476
  total_files_found,
381
477
  total_bytes,
382
478
  total_lines,
383
- used_bytes: total_used_bytes,
479
+ covered_bytes: total_used_bytes,
384
480
  covered_lines: total_covered_lines,
385
- unused_bytes: total_unused_bytes,
481
+ uncovered_bytes: total_unused_bytes,
386
482
  uncovered_lines: total_uncovered_lines,
387
483
  byte_coverage_ratio: ratio(total_used_bytes, total_bytes),
388
484
  line_coverage_ratio: ratio(total_covered_lines, total_lines),
@@ -417,9 +513,9 @@ function validate_min_file_line_coverage(actual, expected) {
417
513
  expected
418
514
  };
419
515
  }
420
- async function program({ min_file_coverage, min_file_line_coverage }, coverage_data) {
516
+ function program({ min_file_coverage, min_file_line_coverage }, coverage_data) {
421
517
  if (coverage_data.length === 0) throw new MissingDataError();
422
- let coverage = await calculate_coverage(coverage_data);
518
+ let coverage = calculate_coverage(coverage_data);
423
519
  let min_line_coverage_result = validate_min_line_coverage(coverage.line_coverage_ratio, min_file_coverage);
424
520
  let min_file_line_coverage_result = validate_min_file_line_coverage(Math.min(...coverage.coverage_per_stylesheet.map((sheet) => sheet.line_coverage_ratio)), min_file_line_coverage);
425
521
  return {
@@ -451,48 +547,54 @@ async function read(coverage_dir) {
451
547
  function indent(line) {
452
548
  return (line || "").replace(/^\t+/, (tabs) => " ".repeat(tabs.length * 4));
453
549
  }
454
- function print({ report, context }, params) {
455
- if (report.min_line_coverage.ok) console.log(`${styleText(["bold", "green"], "Success")}: total line coverage is ${(report.min_line_coverage.actual * 100).toFixed(2)}%`);
456
- else console.error(`${styleText(["bold", "red"], "Failed")}: line coverage is ${(report.min_line_coverage.actual * 100).toFixed(2)}% which is lower than the threshold of ${report.min_line_coverage.expected}`);
550
+ let line_number = (num, covered = true) => `${num.toString().padStart(5, " ")} ${covered ? "│" : "━"} `;
551
+ function percentage(ratio$1, decimals = 2) {
552
+ return `${(ratio$1 * 100).toFixed(ratio$1 === 1 ? 0 : decimals)}%`;
553
+ }
554
+ function print_lines({ report, context }, params, { styleText: styleText$1, print_width }) {
555
+ let output = [];
556
+ if (report.min_line_coverage.ok) output.push(`${styleText$1(["bold", "green"], "Success")}: total line coverage is ${percentage(report.min_line_coverage.actual)}`);
557
+ else output.push(`${styleText$1(["bold", "red"], "Failed")}: line coverage is ${percentage(report.min_line_coverage.actual)}% which is lower than the threshold of ${report.min_line_coverage.expected}`);
457
558
  if (report.min_file_line_coverage.expected !== void 0) {
458
559
  let { expected, actual, ok: ok$1 } = report.min_file_line_coverage;
459
- if (ok$1) console.log(`${styleText(["bold", "green"], "Success")}: all files pass minimum line coverage of ${expected * 100}%`);
560
+ if (ok$1) output.push(`${styleText$1(["bold", "green"], "Success")}: all files pass minimum line coverage of ${percentage(expected)}`);
460
561
  else {
461
562
  let num_files_failed = context.coverage.coverage_per_stylesheet.filter((sheet) => sheet.line_coverage_ratio < expected).length;
462
- console.error(`${styleText(["bold", "red"], "Failed")}: ${num_files_failed} files do not meet the minimum line coverage of ${expected * 100}% (minimum coverage was ${(actual * 100).toFixed(2)}%)`);
463
- if (params["show-uncovered"] === "none") console.log(` Hint: set --show-uncovered=violations to see which files didn't pass`);
563
+ output.push(`${styleText$1(["bold", "red"], "Failed")}: ${num_files_failed} ${num_files_failed === 1 ? "file does" : "files do"} not meet the minimum line coverage of ${percentage(expected)} (minimum coverage was ${percentage(actual)})`);
564
+ if (params["show-uncovered"] === "none") output.push(` Hint: set --show-uncovered=violations to see which files didn't pass`);
464
565
  }
465
566
  }
466
567
  if (params["show-uncovered"] !== "none") {
467
568
  const NUM_LEADING_LINES = 3;
468
569
  const NUM_TRAILING_LINES = NUM_LEADING_LINES;
469
- let terminal_width = process.stdout.columns || 80;
470
- let line_number = (num, covered = true) => `${num.toString().padStart(5, " ")} ${covered ? "│" : "━"} `;
570
+ print_width = print_width ?? 80;
471
571
  let min_file_line_coverage = report.min_file_line_coverage.expected;
472
572
  for (let sheet of context.coverage.coverage_per_stylesheet.sort((a, b) => a.line_coverage_ratio - b.line_coverage_ratio)) if (sheet.line_coverage_ratio !== 1 && params["show-uncovered"] === "all" || min_file_line_coverage !== void 0 && min_file_line_coverage !== 0 && sheet.line_coverage_ratio < min_file_line_coverage && params["show-uncovered"] === "violations") {
473
- console.log();
474
- console.log(styleText("dim", "─".repeat(terminal_width)));
475
- console.log(sheet.url);
476
- console.log(`Coverage: ${(sheet.line_coverage_ratio * 100).toFixed(2)}%, ${sheet.covered_lines}/${sheet.total_lines} lines covered`);
573
+ output.push();
574
+ output.push(styleText$1("dim", "─".repeat(print_width)));
575
+ output.push(sheet.url);
576
+ output.push(`Coverage: ${percentage(sheet.line_coverage_ratio)}, ${sheet.covered_lines}/${sheet.total_lines} lines covered`);
477
577
  if (min_file_line_coverage && min_file_line_coverage !== 0 && sheet.line_coverage_ratio < min_file_line_coverage) {
478
578
  let lines_to_cover = min_file_line_coverage * sheet.total_lines - sheet.covered_lines;
479
- console.log(`Tip: cover ${Math.ceil(lines_to_cover)} more lines to meet the file threshold of ${min_file_line_coverage * 100}%`);
579
+ output.push(`Tip: cover ${Math.ceil(lines_to_cover)} more ${lines_to_cover === 1 ? "line" : "lines"} to meet the file threshold of ${percentage(min_file_line_coverage)}`);
480
580
  }
481
- console.log(styleText("dim", "─".repeat(terminal_width)));
581
+ output.push(styleText$1("dim", "─".repeat(print_width)));
482
582
  let lines = sheet.text.split("\n");
483
- let line_coverage = sheet.line_coverage;
484
- for (let i = 0; i < lines.length; i++) {
485
- if (line_coverage[i] === 1) continue;
486
- for (let j = i - NUM_LEADING_LINES; j < i; j++) if (j >= 0) console.log(styleText("dim", line_number(j)), styleText("dim", indent(lines[j])));
487
- while (line_coverage[i] === 0) {
488
- console.log(styleText("red", line_number(i, false)), indent(lines[i]));
489
- i++;
490
- }
491
- for (let end = i + NUM_TRAILING_LINES; i < end && i < lines.length; i++) console.log(styleText("dim", line_number(i)), styleText("dim", indent(lines[i])));
492
- console.log();
583
+ for (let chunk of sheet.chunks.filter((chunk$1) => !chunk$1.is_covered)) {
584
+ for (let x = Math.max(chunk.start_line - NUM_LEADING_LINES, 1); x < chunk.start_line; x++) output.push([styleText$1("dim", line_number(x)), styleText$1("dim", indent(lines[x - 1]))].join(""));
585
+ for (let i = chunk.start_line; i <= chunk.end_line; i++) output.push([styleText$1("red", line_number(i, false)), indent(lines[i - 1])].join(""));
586
+ for (let y = chunk.end_line + 1; y < Math.min(chunk.end_line + NUM_TRAILING_LINES, lines.length); y++) output.push([styleText$1("dim", line_number(y)), styleText$1("dim", indent(lines[y - 1]))].join(""));
587
+ output.push();
493
588
  }
494
589
  }
495
590
  }
591
+ return output;
592
+ }
593
+ function print(report, params) {
594
+ for (let line of print_lines(report, params, {
595
+ styleText,
596
+ print_width: process.stdout.columns
597
+ })) console.log(line);
496
598
  }
497
599
 
498
600
  //#endregion
@@ -556,13 +658,13 @@ function print$1({ report, context }, params) {
556
658
  async function cli(cli_args) {
557
659
  let params = validate_arguments(parse_arguments(cli_args));
558
660
  let coverage_data = await read(params["coverage-dir"]);
559
- let report = await program({
661
+ let report = program({
560
662
  min_file_coverage: params["min-line-coverage"],
561
663
  min_file_line_coverage: params["min-file-line-coverage"]
562
664
  }, coverage_data);
563
665
  if (report.report.ok === false) process.exitCode = 1;
564
- if (params.reporter === "pretty") print(report, params);
565
- else if (params.reporter === "tap") print$1(report, params);
666
+ if (params.reporter === "tap") return print$1(report, params);
667
+ return print(report, params);
566
668
  }
567
669
  try {
568
670
  await cli(process.argv.slice(2));
package/dist/index.d.ts CHANGED
@@ -24,10 +24,28 @@ declare function parse_coverage(input: string): {
24
24
  }[];
25
25
  }[];
26
26
  //#endregion
27
+ //#region src/lib/chunkify.d.ts
28
+ type Chunk = {
29
+ start_offset: number;
30
+ end_offset: number;
31
+ is_covered: boolean;
32
+ };
33
+ type ChunkedCoverage = Omit<Coverage, 'ranges'> & {
34
+ chunks: Chunk[];
35
+ };
36
+ //#endregion
37
+ //#region src/lib/prettify.d.ts
38
+ type PrettifiedChunk = ChunkedCoverage['chunks'][number] & {
39
+ start_line: number;
40
+ end_line: number;
41
+ total_lines: number;
42
+ css: string;
43
+ };
44
+ //#endregion
27
45
  //#region src/lib/index.d.ts
28
46
  type CoverageData = {
29
- unused_bytes: number;
30
- used_bytes: number;
47
+ uncovered_bytes: number;
48
+ covered_bytes: number;
31
49
  total_bytes: number;
32
50
  line_coverage_ratio: number;
33
51
  byte_coverage_ratio: number;
@@ -38,14 +56,7 @@ type CoverageData = {
38
56
  type StylesheetCoverage = CoverageData & {
39
57
  url: string;
40
58
  text: string;
41
- ranges: Range[];
42
- line_coverage: Uint8Array;
43
- chunks: {
44
- is_covered: boolean;
45
- start_line: number;
46
- end_line: number;
47
- total_lines: number;
48
- }[];
59
+ chunks: PrettifiedChunk[];
49
60
  };
50
61
  type CoverageResult = CoverageData & {
51
62
  total_files_found: number;
@@ -63,6 +74,6 @@ type CoverageResult = CoverageData & {
63
74
  * 4. Calculate used/unused CSS bytes (fastest path, no inspection of the actual CSS needed)
64
75
  * 5. Calculate line-coverage, byte-coverage per stylesheet
65
76
  */
66
- declare function calculate_coverage(coverage: Coverage[]): Promise<CoverageResult>;
77
+ declare function calculate_coverage(coverage: Coverage[]): CoverageResult;
67
78
  //#endregion
68
79
  export { type Coverage, CoverageData, CoverageResult, type Range, StylesheetCoverage, calculate_coverage, parse_coverage };