@projectwallace/css-code-coverage 0.7.0 → 0.8.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 +176 -143
- package/dist/index.d.ts +0 -11
- package/dist/index.js +89 -119
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -13,7 +13,8 @@ const show_uncovered_options = {
|
|
|
13
13
|
};
|
|
14
14
|
const reporters = {
|
|
15
15
|
pretty: "pretty",
|
|
16
|
-
tap: "tap"
|
|
16
|
+
tap: "tap",
|
|
17
|
+
json: "json"
|
|
17
18
|
};
|
|
18
19
|
let CoverageDirSchema = v.pipe(v.string(), v.nonEmpty());
|
|
19
20
|
let RatioPercentageSchema = v.pipe(v.string(), v.transform(Number), v.number(), v.minValue(0), v.maxValue(1));
|
|
@@ -21,8 +22,8 @@ let ShowUncoveredSchema = v.pipe(v.string(), v.enum(show_uncovered_options));
|
|
|
21
22
|
let ReporterSchema = v.pipe(v.string(), v.enum(reporters));
|
|
22
23
|
let CliArgumentsSchema = v.object({
|
|
23
24
|
"coverage-dir": CoverageDirSchema,
|
|
24
|
-
"min-
|
|
25
|
-
"min-file-
|
|
25
|
+
"min-coverage": RatioPercentageSchema,
|
|
26
|
+
"min-file-coverage": v.optional(RatioPercentageSchema),
|
|
26
27
|
"show-uncovered": v.optional(ShowUncoveredSchema, show_uncovered_options.violations),
|
|
27
28
|
reporter: v.optional(ReporterSchema, reporters.pretty)
|
|
28
29
|
});
|
|
@@ -44,11 +45,10 @@ function validate_arguments(args) {
|
|
|
44
45
|
function parse_arguments(args) {
|
|
45
46
|
let { values } = parseArgs({
|
|
46
47
|
args,
|
|
47
|
-
allowPositionals: true,
|
|
48
48
|
options: {
|
|
49
49
|
"coverage-dir": { type: "string" },
|
|
50
|
-
"min-
|
|
51
|
-
"min-file-
|
|
50
|
+
"min-coverage": { type: "string" },
|
|
51
|
+
"min-file-coverage": {
|
|
52
52
|
type: "string",
|
|
53
53
|
default: "0"
|
|
54
54
|
},
|
|
@@ -90,19 +90,20 @@ function parse_coverage(input) {
|
|
|
90
90
|
|
|
91
91
|
//#endregion
|
|
92
92
|
//#region src/lib/chunkify.ts
|
|
93
|
+
const WHITESPACE_ONLY_REGEX = /^\s+$/;
|
|
93
94
|
function merge(stylesheet) {
|
|
94
95
|
let new_chunks = [];
|
|
95
96
|
let previous_chunk;
|
|
96
97
|
for (let i = 0; i < stylesheet.chunks.length; i++) {
|
|
97
98
|
let chunk = stylesheet.chunks.at(i);
|
|
98
|
-
if (
|
|
99
|
+
if (WHITESPACE_ONLY_REGEX.test(stylesheet.text.slice(chunk.start_offset, chunk.end_offset))) continue;
|
|
99
100
|
let latest_chunk = new_chunks.at(-1);
|
|
100
101
|
if (i > 0 && previous_chunk && latest_chunk) {
|
|
101
102
|
if (previous_chunk.is_covered === chunk.is_covered) {
|
|
102
103
|
latest_chunk.end_offset = chunk.end_offset;
|
|
103
104
|
previous_chunk = chunk;
|
|
104
105
|
continue;
|
|
105
|
-
} else if (
|
|
106
|
+
} else if (WHITESPACE_ONLY_REGEX.test(stylesheet.text.slice(chunk.start_offset, chunk.end_offset)) || chunk.end_offset === chunk.start_offset) {
|
|
106
107
|
latest_chunk.end_offset = chunk.end_offset;
|
|
107
108
|
continue;
|
|
108
109
|
}
|
|
@@ -152,10 +153,10 @@ function prettify(stylesheet) {
|
|
|
152
153
|
let line = 1;
|
|
153
154
|
let offset = 0;
|
|
154
155
|
let pretty_chunks = stylesheet.chunks.map((chunk, index) => {
|
|
155
|
-
let css = format(stylesheet.text.
|
|
156
|
+
let css = format(stylesheet.text.substring(chunk.start_offset, chunk.end_offset - 1)).trim();
|
|
156
157
|
if (chunk.is_covered) {
|
|
157
|
-
let
|
|
158
|
-
if (index === 0) css = css + (
|
|
158
|
+
let is_last_chunk = index === stylesheet.chunks.length - 1;
|
|
159
|
+
if (index === 0) css = css + (is_last_chunk ? "" : "\n");
|
|
159
160
|
else if (index === stylesheet.chunks.length - 1) css = "\n" + css;
|
|
160
161
|
else css = "\n" + css + "\n";
|
|
161
162
|
}
|
|
@@ -185,65 +186,46 @@ function prettify(stylesheet) {
|
|
|
185
186
|
|
|
186
187
|
//#endregion
|
|
187
188
|
//#region src/lib/decuplicate.ts
|
|
188
|
-
function
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
189
|
+
function merge_ranges(ranges) {
|
|
190
|
+
if (ranges.length === 0) return [];
|
|
191
|
+
ranges.sort((a, b) => a.start - b.start);
|
|
192
|
+
let merged = [ranges[0]];
|
|
193
|
+
for (let r of ranges.slice(1)) {
|
|
194
|
+
let last = merged.at(-1);
|
|
195
|
+
if (last && r.start <= last.end + 1) {
|
|
196
|
+
if (r.end > last.end) last.end = r.end;
|
|
197
|
+
} else merged.push({
|
|
198
|
+
start: r.start,
|
|
199
|
+
end: r.end
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return merged;
|
|
203
|
+
}
|
|
204
|
+
function merge_entry_ranges(sheet, entry) {
|
|
205
|
+
if (!sheet) return {
|
|
206
|
+
url: entry.url,
|
|
207
|
+
ranges: [...entry.ranges]
|
|
208
|
+
};
|
|
209
|
+
let seen = new Set(sheet.ranges.map((r) => `${r.start}:${r.end}`));
|
|
210
|
+
for (let range of entry.ranges) {
|
|
211
|
+
let id = `${range.start}:${range.end}`;
|
|
212
|
+
if (!seen.has(id)) {
|
|
213
|
+
seen.add(id);
|
|
214
|
+
sheet.ranges.push({ ...range });
|
|
212
215
|
}
|
|
213
|
-
new_ranges.add(range);
|
|
214
216
|
}
|
|
215
|
-
return
|
|
217
|
+
return sheet;
|
|
216
218
|
}
|
|
217
|
-
/**
|
|
218
|
-
* @description
|
|
219
|
-
* prerequisites
|
|
220
|
-
* - we check each stylesheet content only once (to avoid counting the same content multiple times)
|
|
221
|
-
* - if a duplicate stylesheet enters the room, we add it's ranges to the existing stylesheet's ranges
|
|
222
|
-
* - only bytes of deduplicated stylesheets are counted
|
|
223
|
-
*/
|
|
224
219
|
function deduplicate_entries(entries) {
|
|
225
|
-
let
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
let found = false;
|
|
232
|
-
for (let checked_range of ranges) if (checked_range.start === range.start && checked_range.end === range.end) {
|
|
233
|
-
found = true;
|
|
234
|
-
break;
|
|
235
|
-
}
|
|
236
|
-
if (!found) ranges.push(range);
|
|
237
|
-
}
|
|
238
|
-
} else checked_stylesheets.set(text, {
|
|
239
|
-
url: entry.url,
|
|
240
|
-
ranges: entry.ranges
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
return Array.from(checked_stylesheets, ([text, { url, ranges }]) => ({
|
|
220
|
+
let grouped = entries.reduce((acc, entry) => {
|
|
221
|
+
let key = entry.text;
|
|
222
|
+
acc[key] = merge_entry_ranges(acc[key], entry);
|
|
223
|
+
return acc;
|
|
224
|
+
}, Object.create(null));
|
|
225
|
+
return Object.entries(grouped).map(([text, { url, ranges }]) => ({
|
|
244
226
|
text,
|
|
245
227
|
url,
|
|
246
|
-
ranges:
|
|
228
|
+
ranges: merge_ranges(ranges)
|
|
247
229
|
}));
|
|
248
230
|
}
|
|
249
231
|
|
|
@@ -336,31 +318,31 @@ function is_js_like(text) {
|
|
|
336
318
|
return false;
|
|
337
319
|
}
|
|
338
320
|
}
|
|
339
|
-
function filter_coverage(
|
|
340
|
-
let
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
if (is_css_like(entry.text) && !is_js_like(entry.text)) result.push({
|
|
321
|
+
function filter_coverage(acc, entry) {
|
|
322
|
+
let extension = ext(entry.url).toLowerCase();
|
|
323
|
+
if (extension === "js") return acc;
|
|
324
|
+
if (extension === "css") {
|
|
325
|
+
acc.push(entry);
|
|
326
|
+
return acc;
|
|
327
|
+
}
|
|
328
|
+
if (is_html(entry.text)) {
|
|
329
|
+
let { css, ranges } = remap_html(entry.text, entry.ranges);
|
|
330
|
+
acc.push({
|
|
331
|
+
url: entry.url,
|
|
332
|
+
text: css,
|
|
333
|
+
ranges
|
|
334
|
+
});
|
|
335
|
+
return acc;
|
|
336
|
+
}
|
|
337
|
+
if (is_css_like(entry.text) && !is_js_like(entry.text)) {
|
|
338
|
+
acc.push({
|
|
358
339
|
url: entry.url,
|
|
359
340
|
text: entry.text,
|
|
360
341
|
ranges: entry.ranges
|
|
361
342
|
});
|
|
343
|
+
return acc;
|
|
362
344
|
}
|
|
363
|
-
return
|
|
345
|
+
return acc;
|
|
364
346
|
}
|
|
365
347
|
|
|
366
348
|
//#endregion
|
|
@@ -368,38 +350,37 @@ function filter_coverage(coverage) {
|
|
|
368
350
|
const AT_SIGN = 64;
|
|
369
351
|
const LONGEST_ATRULE_NAME = 28;
|
|
370
352
|
function extend_ranges(coverage) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
if (next_char$1 === "{") range.end = range.end + 1;
|
|
388
|
-
break;
|
|
353
|
+
let { ranges, url, text } = coverage;
|
|
354
|
+
return {
|
|
355
|
+
text,
|
|
356
|
+
ranges: ranges.map((range, index) => {
|
|
357
|
+
let prev_range = ranges[index - 1];
|
|
358
|
+
for (let i = range.start; i >= range.start - LONGEST_ATRULE_NAME; i--) {
|
|
359
|
+
if (prev_range && prev_range.end > i) break;
|
|
360
|
+
let char_position = i;
|
|
361
|
+
if (text.charCodeAt(char_position) === AT_SIGN) {
|
|
362
|
+
range.start = char_position;
|
|
363
|
+
let next_offset = range.end;
|
|
364
|
+
let next_char$1 = text.charAt(next_offset);
|
|
365
|
+
while (/\s/.test(next_char$1)) {
|
|
366
|
+
next_offset++;
|
|
367
|
+
next_char$1 = text.charAt(next_offset);
|
|
389
368
|
}
|
|
369
|
+
if (next_char$1 === "{") range.end = range.end + 1;
|
|
370
|
+
break;
|
|
390
371
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
}
|
|
372
|
+
}
|
|
373
|
+
let offset = range.end;
|
|
374
|
+
let next_char = text.charAt(offset);
|
|
375
|
+
while (/\s/.test(next_char)) {
|
|
376
|
+
offset++;
|
|
377
|
+
next_char = text.charAt(offset);
|
|
378
|
+
}
|
|
379
|
+
if (next_char === "}") range.end = offset + 1;
|
|
380
|
+
return range;
|
|
381
|
+
}),
|
|
382
|
+
url
|
|
383
|
+
};
|
|
403
384
|
}
|
|
404
385
|
|
|
405
386
|
//#endregion
|
|
@@ -442,20 +423,9 @@ function calculate_stylesheet_coverage({ text, url, chunks }) {
|
|
|
442
423
|
chunks
|
|
443
424
|
};
|
|
444
425
|
}
|
|
445
|
-
/**
|
|
446
|
-
* @description
|
|
447
|
-
* CSS Code Coverage calculation
|
|
448
|
-
*
|
|
449
|
-
* These are the steps performed to calculate coverage:
|
|
450
|
-
* 1. Filter eligible files / validate input
|
|
451
|
-
* 2. Prettify the CSS dicovered in each Coverage and update their ranges
|
|
452
|
-
* 3. De-duplicate Coverages: merge all ranges for CSS sources occurring multiple times
|
|
453
|
-
* 4. Calculate used/unused CSS bytes (fastest path, no inspection of the actual CSS needed)
|
|
454
|
-
* 5. Calculate line-coverage, byte-coverage per stylesheet
|
|
455
|
-
*/
|
|
456
426
|
function calculate_coverage(coverage) {
|
|
457
427
|
let total_files_found = coverage.length;
|
|
458
|
-
let coverage_per_stylesheet =
|
|
428
|
+
let coverage_per_stylesheet = coverage.reduce((acc, entry) => filter_coverage(acc, entry), []).reduce((entries, entry) => deduplicate_entries(entries.concat(entry)), []).map((coverage$1) => extend_ranges(coverage$1)).map((sheet) => chunkify(sheet)).map((sheet) => prettify(sheet)).map((stylesheet) => calculate_stylesheet_coverage(stylesheet));
|
|
459
429
|
let { total_lines, total_covered_lines, total_uncovered_lines, total_bytes, total_used_bytes, total_unused_bytes } = coverage_per_stylesheet.reduce((totals, sheet) => {
|
|
460
430
|
totals.total_lines += sheet.total_lines;
|
|
461
431
|
totals.total_covered_lines += sheet.covered_lines;
|
|
@@ -489,11 +459,6 @@ function calculate_coverage(coverage) {
|
|
|
489
459
|
|
|
490
460
|
//#endregion
|
|
491
461
|
//#region src/cli/program.ts
|
|
492
|
-
var MissingDataError = class extends Error {
|
|
493
|
-
constructor() {
|
|
494
|
-
super("No data to analyze");
|
|
495
|
-
}
|
|
496
|
-
};
|
|
497
462
|
function validate_min_line_coverage(actual, expected) {
|
|
498
463
|
return {
|
|
499
464
|
ok: actual >= expected,
|
|
@@ -513,17 +478,16 @@ function validate_min_file_line_coverage(actual, expected) {
|
|
|
513
478
|
expected
|
|
514
479
|
};
|
|
515
480
|
}
|
|
516
|
-
function program({
|
|
517
|
-
if (coverage_data.length === 0) throw new MissingDataError();
|
|
481
|
+
function program({ min_coverage, min_file_coverage }, coverage_data) {
|
|
518
482
|
let coverage = calculate_coverage(coverage_data);
|
|
519
|
-
let
|
|
520
|
-
let
|
|
483
|
+
let min_coverage_result = validate_min_line_coverage(coverage.line_coverage_ratio, min_coverage);
|
|
484
|
+
let min_file_coverage_result = validate_min_file_line_coverage(Math.min(...coverage.coverage_per_stylesheet.map((sheet) => sheet.line_coverage_ratio)), min_file_coverage);
|
|
521
485
|
return {
|
|
522
486
|
context: { coverage },
|
|
523
487
|
report: {
|
|
524
|
-
ok:
|
|
525
|
-
min_line_coverage:
|
|
526
|
-
min_file_line_coverage:
|
|
488
|
+
ok: min_coverage_result.ok && min_file_coverage_result.ok,
|
|
489
|
+
min_line_coverage: min_coverage_result,
|
|
490
|
+
min_file_line_coverage: min_file_coverage_result
|
|
527
491
|
}
|
|
528
492
|
};
|
|
529
493
|
}
|
|
@@ -554,7 +518,12 @@ function percentage(ratio$1, decimals = 2) {
|
|
|
554
518
|
function print_lines({ report, context }, params, { styleText: styleText$1, print_width }) {
|
|
555
519
|
let output = [];
|
|
556
520
|
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
|
|
521
|
+
else {
|
|
522
|
+
let { actual, expected } = report.min_line_coverage;
|
|
523
|
+
output.push(`${styleText$1(["bold", "red"], "Failed")}: line coverage is ${percentage(actual)}% which is lower than the threshold of ${expected}`);
|
|
524
|
+
let lines_to_cover = expected * context.coverage.total_lines - context.coverage.covered_lines;
|
|
525
|
+
output.push(`Tip: cover ${Math.ceil(lines_to_cover)} more ${lines_to_cover === 1 ? "line" : "lines"} to meet the threshold of ${percentage(expected)}`);
|
|
526
|
+
}
|
|
558
527
|
if (report.min_file_line_coverage.expected !== void 0) {
|
|
559
528
|
let { expected, actual, ok: ok$1 } = report.min_file_line_coverage;
|
|
560
529
|
if (ok$1) output.push(`${styleText$1(["bold", "green"], "Success")}: all files pass minimum line coverage of ${percentage(expected)}`);
|
|
@@ -569,6 +538,7 @@ function print_lines({ report, context }, params, { styleText: styleText$1, prin
|
|
|
569
538
|
const NUM_TRAILING_LINES = NUM_LEADING_LINES;
|
|
570
539
|
print_width = print_width ?? 80;
|
|
571
540
|
let min_file_line_coverage = report.min_file_line_coverage.expected;
|
|
541
|
+
output.push();
|
|
572
542
|
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") {
|
|
573
543
|
output.push();
|
|
574
544
|
output.push(styleText$1("dim", "─".repeat(print_width)));
|
|
@@ -591,10 +561,11 @@ function print_lines({ report, context }, params, { styleText: styleText$1, prin
|
|
|
591
561
|
return output;
|
|
592
562
|
}
|
|
593
563
|
function print(report, params) {
|
|
564
|
+
let logger = report.report.ok ? console.log : console.error;
|
|
594
565
|
for (let line of print_lines(report, params, {
|
|
595
566
|
styleText,
|
|
596
567
|
print_width: process.stdout.columns
|
|
597
|
-
}))
|
|
568
|
+
})) logger(line);
|
|
598
569
|
}
|
|
599
570
|
|
|
600
571
|
//#endregion
|
|
@@ -609,7 +580,7 @@ function ok(n, description) {
|
|
|
609
580
|
console.log(`ok ${n} ${description ? `- ${description}` : ""}`);
|
|
610
581
|
}
|
|
611
582
|
function not_ok(n, description) {
|
|
612
|
-
console.
|
|
583
|
+
console.error(`not ok ${n} ${description ? `- ${description}` : ""}`);
|
|
613
584
|
}
|
|
614
585
|
function meta(data) {
|
|
615
586
|
console.log(" ---");
|
|
@@ -653,17 +624,79 @@ function print$1({ report, context }, params) {
|
|
|
653
624
|
}
|
|
654
625
|
}
|
|
655
626
|
|
|
627
|
+
//#endregion
|
|
628
|
+
//#region src/cli/reporters/json.ts
|
|
629
|
+
function prepare({ report, context }, params) {
|
|
630
|
+
context.coverage.coverage_per_stylesheet = context.coverage.coverage_per_stylesheet.filter((sheet) => {
|
|
631
|
+
if (params["show-uncovered"] === "violations" && report.min_file_line_coverage.expected !== void 0 && sheet.line_coverage_ratio < report.min_file_line_coverage.expected) return true;
|
|
632
|
+
if (params["show-uncovered"] === "all" && sheet.line_coverage_ratio < 1) return true;
|
|
633
|
+
return false;
|
|
634
|
+
});
|
|
635
|
+
return {
|
|
636
|
+
report,
|
|
637
|
+
context
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
function print$2({ report, context }, params) {
|
|
641
|
+
let logger = report.ok ? console.log : console.error;
|
|
642
|
+
let data = prepare({
|
|
643
|
+
context,
|
|
644
|
+
report
|
|
645
|
+
}, params);
|
|
646
|
+
logger(JSON.stringify(data));
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
//#endregion
|
|
650
|
+
//#region src/cli/help.ts
|
|
651
|
+
function help() {
|
|
652
|
+
return `
|
|
653
|
+
${styleText(["bold"], "USAGE")}
|
|
654
|
+
$ css-coverage --coverage-dir=<dir> --min-coverage=<number> [options]
|
|
655
|
+
|
|
656
|
+
${styleText("bold", "OPTIONS")}
|
|
657
|
+
Required:
|
|
658
|
+
--coverage-dir Where your Coverage JSON files are
|
|
659
|
+
--min-coverage Minimum overall CSS coverage [0-1]
|
|
660
|
+
|
|
661
|
+
Optional:
|
|
662
|
+
--min-file-coverage Minimal coverage per file [0-1]
|
|
663
|
+
|
|
664
|
+
--show-uncovered Which files to show when not meeting
|
|
665
|
+
the --min-file-line-coverage threshold
|
|
666
|
+
• violations [default] ${styleText("dim", "show under-threshold files")}
|
|
667
|
+
• all ${styleText("dim", "show partially covered files")}
|
|
668
|
+
• none ${styleText("dim", "do not show files")}
|
|
669
|
+
|
|
670
|
+
--reporter How to show the results
|
|
671
|
+
• pretty [default]
|
|
672
|
+
• tap
|
|
673
|
+
• json
|
|
674
|
+
|
|
675
|
+
${styleText("bold", "EXAMPLES")}
|
|
676
|
+
${styleText("dim", "# analyze all .json files in ./coverage; require 80% overall coverage")}
|
|
677
|
+
$ css-coverage --coverage-dir=./coverage --min-coverage=0.8
|
|
678
|
+
|
|
679
|
+
${styleText("dim", "# Require 50% coverage per file")}
|
|
680
|
+
$ css-coverage --coverage-dir=./coverage --min-coverage=0.8 --min-file-coverage=0.5
|
|
681
|
+
|
|
682
|
+
${styleText("dim", "Report JSON")}
|
|
683
|
+
$ css-coverage --coverage-dir=./coverage --min-coverage=0.8 --reporter=json
|
|
684
|
+
`.trim();
|
|
685
|
+
}
|
|
686
|
+
|
|
656
687
|
//#endregion
|
|
657
688
|
//#region src/cli/cli.ts
|
|
658
689
|
async function cli(cli_args) {
|
|
690
|
+
if (!cli_args || cli_args.length === 0 || cli_args.includes("--help") || cli_args.includes("-h")) return console.log(help());
|
|
659
691
|
let params = validate_arguments(parse_arguments(cli_args));
|
|
660
692
|
let coverage_data = await read(params["coverage-dir"]);
|
|
661
693
|
let report = program({
|
|
662
|
-
|
|
663
|
-
|
|
694
|
+
min_coverage: params["min-coverage"],
|
|
695
|
+
min_file_coverage: params["min-file-coverage"]
|
|
664
696
|
}, coverage_data);
|
|
665
697
|
if (report.report.ok === false) process.exitCode = 1;
|
|
666
698
|
if (params.reporter === "tap") return print$1(report, params);
|
|
699
|
+
if (params.reporter === "json") return print$2(report, params);
|
|
667
700
|
return print(report, params);
|
|
668
701
|
}
|
|
669
702
|
try {
|
package/dist/index.d.ts
CHANGED
|
@@ -63,17 +63,6 @@ type CoverageResult = CoverageData & {
|
|
|
63
63
|
total_stylesheets: number;
|
|
64
64
|
coverage_per_stylesheet: StylesheetCoverage[];
|
|
65
65
|
};
|
|
66
|
-
/**
|
|
67
|
-
* @description
|
|
68
|
-
* CSS Code Coverage calculation
|
|
69
|
-
*
|
|
70
|
-
* These are the steps performed to calculate coverage:
|
|
71
|
-
* 1. Filter eligible files / validate input
|
|
72
|
-
* 2. Prettify the CSS dicovered in each Coverage and update their ranges
|
|
73
|
-
* 3. De-duplicate Coverages: merge all ranges for CSS sources occurring multiple times
|
|
74
|
-
* 4. Calculate used/unused CSS bytes (fastest path, no inspection of the actual CSS needed)
|
|
75
|
-
* 5. Calculate line-coverage, byte-coverage per stylesheet
|
|
76
|
-
*/
|
|
77
66
|
declare function calculate_coverage(coverage: Coverage[]): CoverageResult;
|
|
78
67
|
//#endregion
|
|
79
68
|
export { type Coverage, CoverageData, CoverageResult, type Range, StylesheetCoverage, calculate_coverage, parse_coverage };
|
package/dist/index.js
CHANGED
|
@@ -25,19 +25,20 @@ function parse_coverage(input) {
|
|
|
25
25
|
|
|
26
26
|
//#endregion
|
|
27
27
|
//#region src/lib/chunkify.ts
|
|
28
|
+
const WHITESPACE_ONLY_REGEX = /^\s+$/;
|
|
28
29
|
function merge(stylesheet) {
|
|
29
30
|
let new_chunks = [];
|
|
30
31
|
let previous_chunk;
|
|
31
32
|
for (let i = 0; i < stylesheet.chunks.length; i++) {
|
|
32
33
|
let chunk = stylesheet.chunks.at(i);
|
|
33
|
-
if (
|
|
34
|
+
if (WHITESPACE_ONLY_REGEX.test(stylesheet.text.slice(chunk.start_offset, chunk.end_offset))) continue;
|
|
34
35
|
let latest_chunk = new_chunks.at(-1);
|
|
35
36
|
if (i > 0 && previous_chunk && latest_chunk) {
|
|
36
37
|
if (previous_chunk.is_covered === chunk.is_covered) {
|
|
37
38
|
latest_chunk.end_offset = chunk.end_offset;
|
|
38
39
|
previous_chunk = chunk;
|
|
39
40
|
continue;
|
|
40
|
-
} else if (
|
|
41
|
+
} else if (WHITESPACE_ONLY_REGEX.test(stylesheet.text.slice(chunk.start_offset, chunk.end_offset)) || chunk.end_offset === chunk.start_offset) {
|
|
41
42
|
latest_chunk.end_offset = chunk.end_offset;
|
|
42
43
|
continue;
|
|
43
44
|
}
|
|
@@ -87,10 +88,10 @@ function prettify(stylesheet) {
|
|
|
87
88
|
let line = 1;
|
|
88
89
|
let offset = 0;
|
|
89
90
|
let pretty_chunks = stylesheet.chunks.map((chunk, index) => {
|
|
90
|
-
let css = format(stylesheet.text.
|
|
91
|
+
let css = format(stylesheet.text.substring(chunk.start_offset, chunk.end_offset - 1)).trim();
|
|
91
92
|
if (chunk.is_covered) {
|
|
92
|
-
let
|
|
93
|
-
if (index === 0) css = css + (
|
|
93
|
+
let is_last_chunk = index === stylesheet.chunks.length - 1;
|
|
94
|
+
if (index === 0) css = css + (is_last_chunk ? "" : "\n");
|
|
94
95
|
else if (index === stylesheet.chunks.length - 1) css = "\n" + css;
|
|
95
96
|
else css = "\n" + css + "\n";
|
|
96
97
|
}
|
|
@@ -120,65 +121,46 @@ function prettify(stylesheet) {
|
|
|
120
121
|
|
|
121
122
|
//#endregion
|
|
122
123
|
//#region src/lib/decuplicate.ts
|
|
123
|
-
function
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
function merge_ranges(ranges) {
|
|
125
|
+
if (ranges.length === 0) return [];
|
|
126
|
+
ranges.sort((a, b) => a.start - b.start);
|
|
127
|
+
let merged = [ranges[0]];
|
|
128
|
+
for (let r of ranges.slice(1)) {
|
|
129
|
+
let last = merged.at(-1);
|
|
130
|
+
if (last && r.start <= last.end + 1) {
|
|
131
|
+
if (r.end > last.end) last.end = r.end;
|
|
132
|
+
} else merged.push({
|
|
133
|
+
start: r.start,
|
|
134
|
+
end: r.end
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return merged;
|
|
128
138
|
}
|
|
129
|
-
function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
new_ranges.delete(processed_range);
|
|
141
|
-
new_ranges.add({
|
|
142
|
-
start: processed_range.start,
|
|
143
|
-
end: range.end
|
|
144
|
-
});
|
|
145
|
-
continue outer;
|
|
146
|
-
}
|
|
139
|
+
function merge_entry_ranges(sheet, entry) {
|
|
140
|
+
if (!sheet) return {
|
|
141
|
+
url: entry.url,
|
|
142
|
+
ranges: [...entry.ranges]
|
|
143
|
+
};
|
|
144
|
+
let seen = new Set(sheet.ranges.map((r) => `${r.start}:${r.end}`));
|
|
145
|
+
for (let range of entry.ranges) {
|
|
146
|
+
let id = `${range.start}:${range.end}`;
|
|
147
|
+
if (!seen.has(id)) {
|
|
148
|
+
seen.add(id);
|
|
149
|
+
sheet.ranges.push({ ...range });
|
|
147
150
|
}
|
|
148
|
-
new_ranges.add(range);
|
|
149
151
|
}
|
|
150
|
-
return
|
|
152
|
+
return sheet;
|
|
151
153
|
}
|
|
152
|
-
/**
|
|
153
|
-
* @description
|
|
154
|
-
* prerequisites
|
|
155
|
-
* - we check each stylesheet content only once (to avoid counting the same content multiple times)
|
|
156
|
-
* - if a duplicate stylesheet enters the room, we add it's ranges to the existing stylesheet's ranges
|
|
157
|
-
* - only bytes of deduplicated stylesheets are counted
|
|
158
|
-
*/
|
|
159
154
|
function deduplicate_entries(entries) {
|
|
160
|
-
let
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
let found = false;
|
|
167
|
-
for (let checked_range of ranges) if (checked_range.start === range.start && checked_range.end === range.end) {
|
|
168
|
-
found = true;
|
|
169
|
-
break;
|
|
170
|
-
}
|
|
171
|
-
if (!found) ranges.push(range);
|
|
172
|
-
}
|
|
173
|
-
} else checked_stylesheets.set(text, {
|
|
174
|
-
url: entry.url,
|
|
175
|
-
ranges: entry.ranges
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
return Array.from(checked_stylesheets, ([text, { url, ranges }]) => ({
|
|
155
|
+
let grouped = entries.reduce((acc, entry) => {
|
|
156
|
+
let key = entry.text;
|
|
157
|
+
acc[key] = merge_entry_ranges(acc[key], entry);
|
|
158
|
+
return acc;
|
|
159
|
+
}, Object.create(null));
|
|
160
|
+
return Object.entries(grouped).map(([text, { url, ranges }]) => ({
|
|
179
161
|
text,
|
|
180
162
|
url,
|
|
181
|
-
ranges:
|
|
163
|
+
ranges: merge_ranges(ranges)
|
|
182
164
|
}));
|
|
183
165
|
}
|
|
184
166
|
|
|
@@ -271,31 +253,31 @@ function is_js_like(text) {
|
|
|
271
253
|
return false;
|
|
272
254
|
}
|
|
273
255
|
}
|
|
274
|
-
function filter_coverage(
|
|
275
|
-
let
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (is_css_like(entry.text) && !is_js_like(entry.text)) result.push({
|
|
256
|
+
function filter_coverage(acc, entry) {
|
|
257
|
+
let extension = ext(entry.url).toLowerCase();
|
|
258
|
+
if (extension === "js") return acc;
|
|
259
|
+
if (extension === "css") {
|
|
260
|
+
acc.push(entry);
|
|
261
|
+
return acc;
|
|
262
|
+
}
|
|
263
|
+
if (is_html(entry.text)) {
|
|
264
|
+
let { css, ranges } = remap_html(entry.text, entry.ranges);
|
|
265
|
+
acc.push({
|
|
266
|
+
url: entry.url,
|
|
267
|
+
text: css,
|
|
268
|
+
ranges
|
|
269
|
+
});
|
|
270
|
+
return acc;
|
|
271
|
+
}
|
|
272
|
+
if (is_css_like(entry.text) && !is_js_like(entry.text)) {
|
|
273
|
+
acc.push({
|
|
293
274
|
url: entry.url,
|
|
294
275
|
text: entry.text,
|
|
295
276
|
ranges: entry.ranges
|
|
296
277
|
});
|
|
278
|
+
return acc;
|
|
297
279
|
}
|
|
298
|
-
return
|
|
280
|
+
return acc;
|
|
299
281
|
}
|
|
300
282
|
|
|
301
283
|
//#endregion
|
|
@@ -303,38 +285,37 @@ function filter_coverage(coverage) {
|
|
|
303
285
|
const AT_SIGN = 64;
|
|
304
286
|
const LONGEST_ATRULE_NAME = 28;
|
|
305
287
|
function extend_ranges(coverage) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
if (next_char$1 === "{") range.end = range.end + 1;
|
|
323
|
-
break;
|
|
288
|
+
let { ranges, url, text } = coverage;
|
|
289
|
+
return {
|
|
290
|
+
text,
|
|
291
|
+
ranges: ranges.map((range, index) => {
|
|
292
|
+
let prev_range = ranges[index - 1];
|
|
293
|
+
for (let i = range.start; i >= range.start - LONGEST_ATRULE_NAME; i--) {
|
|
294
|
+
if (prev_range && prev_range.end > i) break;
|
|
295
|
+
let char_position = i;
|
|
296
|
+
if (text.charCodeAt(char_position) === AT_SIGN) {
|
|
297
|
+
range.start = char_position;
|
|
298
|
+
let next_offset = range.end;
|
|
299
|
+
let next_char$1 = text.charAt(next_offset);
|
|
300
|
+
while (/\s/.test(next_char$1)) {
|
|
301
|
+
next_offset++;
|
|
302
|
+
next_char$1 = text.charAt(next_offset);
|
|
324
303
|
}
|
|
304
|
+
if (next_char$1 === "{") range.end = range.end + 1;
|
|
305
|
+
break;
|
|
325
306
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
307
|
+
}
|
|
308
|
+
let offset = range.end;
|
|
309
|
+
let next_char = text.charAt(offset);
|
|
310
|
+
while (/\s/.test(next_char)) {
|
|
311
|
+
offset++;
|
|
312
|
+
next_char = text.charAt(offset);
|
|
313
|
+
}
|
|
314
|
+
if (next_char === "}") range.end = offset + 1;
|
|
315
|
+
return range;
|
|
316
|
+
}),
|
|
317
|
+
url
|
|
318
|
+
};
|
|
338
319
|
}
|
|
339
320
|
|
|
340
321
|
//#endregion
|
|
@@ -377,20 +358,9 @@ function calculate_stylesheet_coverage({ text, url, chunks }) {
|
|
|
377
358
|
chunks
|
|
378
359
|
};
|
|
379
360
|
}
|
|
380
|
-
/**
|
|
381
|
-
* @description
|
|
382
|
-
* CSS Code Coverage calculation
|
|
383
|
-
*
|
|
384
|
-
* These are the steps performed to calculate coverage:
|
|
385
|
-
* 1. Filter eligible files / validate input
|
|
386
|
-
* 2. Prettify the CSS dicovered in each Coverage and update their ranges
|
|
387
|
-
* 3. De-duplicate Coverages: merge all ranges for CSS sources occurring multiple times
|
|
388
|
-
* 4. Calculate used/unused CSS bytes (fastest path, no inspection of the actual CSS needed)
|
|
389
|
-
* 5. Calculate line-coverage, byte-coverage per stylesheet
|
|
390
|
-
*/
|
|
391
361
|
function calculate_coverage(coverage) {
|
|
392
362
|
let total_files_found = coverage.length;
|
|
393
|
-
let coverage_per_stylesheet =
|
|
363
|
+
let coverage_per_stylesheet = coverage.reduce((acc, entry) => filter_coverage(acc, entry), []).reduce((entries, entry) => deduplicate_entries(entries.concat(entry)), []).map((coverage$1) => extend_ranges(coverage$1)).map((sheet) => chunkify(sheet)).map((sheet) => prettify(sheet)).map((stylesheet) => calculate_stylesheet_coverage(stylesheet));
|
|
394
364
|
let { total_lines, total_covered_lines, total_uncovered_lines, total_bytes, total_used_bytes, total_unused_bytes } = coverage_per_stylesheet.reduce((totals, sheet) => {
|
|
395
365
|
totals.total_lines += sheet.total_lines;
|
|
396
366
|
totals.total_covered_lines += sheet.covered_lines;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@projectwallace/css-code-coverage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Generate useful CSS Code Coverage report from browser-reported coverage",
|
|
5
5
|
"author": "Bart Veneman <bart@projectwallace.com>",
|
|
6
6
|
"repository": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"types": "dist/index.d.ts",
|
|
39
39
|
"scripts": {
|
|
40
|
-
"test": "c8 --reporter=text playwright test",
|
|
40
|
+
"test": "c8 --reporter=text --reporter=lcov playwright test",
|
|
41
41
|
"build": "tsdown",
|
|
42
42
|
"check": "tsc --noEmit",
|
|
43
43
|
"lint": "oxlint --config .oxlintrc.json",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"knip": "knip"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
+
"@codecov/vite-plugin": "^1.9.1",
|
|
48
49
|
"@playwright/test": "^1.56.0",
|
|
49
50
|
"@types/node": "^24.9.2",
|
|
50
51
|
"c8": "^10.1.3",
|