@projectwallace/css-code-coverage 0.6.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/README.md +1 -1
- package/dist/cli.js +84 -32
- package/dist/index.d.ts +1 -1
- package/dist/index.js +50 -9
- package/package.json +5 -4
- package/dist/esm--VCpEgdH.js +0 -11177
- package/dist/esm-CWr4VY0v.js +0 -11013
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ let coverage = await page.coverage.stopCSSCoverage()
|
|
|
53
53
|
// Now we can process it
|
|
54
54
|
import { calculate_coverage } from '@projectwallace/css-code-coverage'
|
|
55
55
|
|
|
56
|
-
let report =
|
|
56
|
+
let report = calculcate_coverage(coverage)
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
### Browser devtools
|
package/dist/cli.js
CHANGED
|
@@ -259,15 +259,43 @@ function ext(url) {
|
|
|
259
259
|
}
|
|
260
260
|
}
|
|
261
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
|
+
|
|
262
291
|
//#endregion
|
|
263
292
|
//#region src/lib/remap-html.ts
|
|
264
|
-
|
|
293
|
+
function get_dom_parser() {
|
|
265
294
|
if (typeof window !== "undefined" && "DOMParser" in window) return new window.DOMParser();
|
|
266
|
-
let { DOMParser } = await import("./esm--VCpEgdH.js");
|
|
267
295
|
return new DOMParser();
|
|
268
296
|
}
|
|
269
|
-
|
|
270
|
-
let doc =
|
|
297
|
+
function remap_html(html, old_ranges) {
|
|
298
|
+
let doc = get_dom_parser().parseFromString(html, "text/html");
|
|
271
299
|
let combined_css = "";
|
|
272
300
|
let new_ranges = [];
|
|
273
301
|
let current_offset = 0;
|
|
@@ -295,7 +323,20 @@ async function remap_html(html, old_ranges) {
|
|
|
295
323
|
function is_html(text) {
|
|
296
324
|
return /<\/?(html|body|head|div|span|script|style)/i.test(text);
|
|
297
325
|
}
|
|
298
|
-
|
|
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) {
|
|
299
340
|
let result = [];
|
|
300
341
|
for (let entry of coverage) {
|
|
301
342
|
let extension = ext(entry.url).toLowerCase();
|
|
@@ -305,7 +346,7 @@ async function filter_coverage(coverage) {
|
|
|
305
346
|
continue;
|
|
306
347
|
}
|
|
307
348
|
if (is_html(entry.text)) {
|
|
308
|
-
let { css, ranges } =
|
|
349
|
+
let { css, ranges } = remap_html(entry.text, entry.ranges);
|
|
309
350
|
result.push({
|
|
310
351
|
url: entry.url,
|
|
311
352
|
text: css,
|
|
@@ -313,7 +354,7 @@ async function filter_coverage(coverage) {
|
|
|
313
354
|
});
|
|
314
355
|
continue;
|
|
315
356
|
}
|
|
316
|
-
result.push({
|
|
357
|
+
if (is_css_like(entry.text) && !is_js_like(entry.text)) result.push({
|
|
317
358
|
url: entry.url,
|
|
318
359
|
text: entry.text,
|
|
319
360
|
ranges: entry.ranges
|
|
@@ -412,9 +453,9 @@ function calculate_stylesheet_coverage({ text, url, chunks }) {
|
|
|
412
453
|
* 4. Calculate used/unused CSS bytes (fastest path, no inspection of the actual CSS needed)
|
|
413
454
|
* 5. Calculate line-coverage, byte-coverage per stylesheet
|
|
414
455
|
*/
|
|
415
|
-
|
|
456
|
+
function calculate_coverage(coverage) {
|
|
416
457
|
let total_files_found = coverage.length;
|
|
417
|
-
let coverage_per_stylesheet = extend_ranges(deduplicate_entries(
|
|
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));
|
|
418
459
|
let { total_lines, total_covered_lines, total_uncovered_lines, total_bytes, total_used_bytes, total_unused_bytes } = coverage_per_stylesheet.reduce((totals, sheet) => {
|
|
419
460
|
totals.total_lines += sheet.total_lines;
|
|
420
461
|
totals.total_covered_lines += sheet.covered_lines;
|
|
@@ -472,9 +513,9 @@ function validate_min_file_line_coverage(actual, expected) {
|
|
|
472
513
|
expected
|
|
473
514
|
};
|
|
474
515
|
}
|
|
475
|
-
|
|
516
|
+
function program({ min_file_coverage, min_file_line_coverage }, coverage_data) {
|
|
476
517
|
if (coverage_data.length === 0) throw new MissingDataError();
|
|
477
|
-
let coverage =
|
|
518
|
+
let coverage = calculate_coverage(coverage_data);
|
|
478
519
|
let min_line_coverage_result = validate_min_line_coverage(coverage.line_coverage_ratio, min_file_coverage);
|
|
479
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);
|
|
480
521
|
return {
|
|
@@ -506,43 +547,54 @@ async function read(coverage_dir) {
|
|
|
506
547
|
function indent(line) {
|
|
507
548
|
return (line || "").replace(/^\t+/, (tabs) => " ".repeat(tabs.length * 4));
|
|
508
549
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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}`);
|
|
512
558
|
if (report.min_file_line_coverage.expected !== void 0) {
|
|
513
559
|
let { expected, actual, ok: ok$1 } = report.min_file_line_coverage;
|
|
514
|
-
if (ok$1)
|
|
560
|
+
if (ok$1) output.push(`${styleText$1(["bold", "green"], "Success")}: all files pass minimum line coverage of ${percentage(expected)}`);
|
|
515
561
|
else {
|
|
516
562
|
let num_files_failed = context.coverage.coverage_per_stylesheet.filter((sheet) => sheet.line_coverage_ratio < expected).length;
|
|
517
|
-
|
|
518
|
-
if (params["show-uncovered"] === "none")
|
|
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`);
|
|
519
565
|
}
|
|
520
566
|
}
|
|
521
567
|
if (params["show-uncovered"] !== "none") {
|
|
522
568
|
const NUM_LEADING_LINES = 3;
|
|
523
569
|
const NUM_TRAILING_LINES = NUM_LEADING_LINES;
|
|
524
|
-
|
|
525
|
-
let line_number = (num, covered = true) => `${num.toString().padStart(5, " ")} ${covered ? "│" : "━"} `;
|
|
570
|
+
print_width = print_width ?? 80;
|
|
526
571
|
let min_file_line_coverage = report.min_file_line_coverage.expected;
|
|
527
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") {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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`);
|
|
532
577
|
if (min_file_line_coverage && min_file_line_coverage !== 0 && sheet.line_coverage_ratio < min_file_line_coverage) {
|
|
533
578
|
let lines_to_cover = min_file_line_coverage * sheet.total_lines - sheet.covered_lines;
|
|
534
|
-
|
|
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)}`);
|
|
535
580
|
}
|
|
536
|
-
|
|
581
|
+
output.push(styleText$1("dim", "─".repeat(print_width)));
|
|
537
582
|
let lines = sheet.text.split("\n");
|
|
538
583
|
for (let chunk of sheet.chunks.filter((chunk$1) => !chunk$1.is_covered)) {
|
|
539
|
-
for (let x = Math.max(chunk.start_line - NUM_LEADING_LINES,
|
|
540
|
-
for (let i = chunk.start_line; i <= chunk.end_line; i++)
|
|
541
|
-
for (let y = chunk.end_line; y < Math.min(chunk.end_line + NUM_TRAILING_LINES, lines.length); y++)
|
|
542
|
-
|
|
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();
|
|
543
588
|
}
|
|
544
589
|
}
|
|
545
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);
|
|
546
598
|
}
|
|
547
599
|
|
|
548
600
|
//#endregion
|
|
@@ -606,13 +658,13 @@ function print$1({ report, context }, params) {
|
|
|
606
658
|
async function cli(cli_args) {
|
|
607
659
|
let params = validate_arguments(parse_arguments(cli_args));
|
|
608
660
|
let coverage_data = await read(params["coverage-dir"]);
|
|
609
|
-
let report =
|
|
661
|
+
let report = program({
|
|
610
662
|
min_file_coverage: params["min-line-coverage"],
|
|
611
663
|
min_file_line_coverage: params["min-file-line-coverage"]
|
|
612
664
|
}, coverage_data);
|
|
613
665
|
if (report.report.ok === false) process.exitCode = 1;
|
|
614
|
-
if (params.reporter === "
|
|
615
|
-
|
|
666
|
+
if (params.reporter === "tap") return print$1(report, params);
|
|
667
|
+
return print(report, params);
|
|
616
668
|
}
|
|
617
669
|
try {
|
|
618
670
|
await cli(process.argv.slice(2));
|
package/dist/index.d.ts
CHANGED
|
@@ -74,6 +74,6 @@ type CoverageResult = CoverageData & {
|
|
|
74
74
|
* 4. Calculate used/unused CSS bytes (fastest path, no inspection of the actual CSS needed)
|
|
75
75
|
* 5. Calculate line-coverage, byte-coverage per stylesheet
|
|
76
76
|
*/
|
|
77
|
-
declare function calculate_coverage(coverage: Coverage[]):
|
|
77
|
+
declare function calculate_coverage(coverage: Coverage[]): CoverageResult;
|
|
78
78
|
//#endregion
|
|
79
79
|
export { type Coverage, CoverageData, CoverageResult, type Range, StylesheetCoverage, calculate_coverage, parse_coverage };
|
package/dist/index.js
CHANGED
|
@@ -194,15 +194,43 @@ function ext(url) {
|
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/lib/html-parser.ts
|
|
199
|
+
/**
|
|
200
|
+
* @description
|
|
201
|
+
* Very, very naive but effective DOMParser.
|
|
202
|
+
* It can only find <style> elements and their .textContent
|
|
203
|
+
*/
|
|
204
|
+
var DOMParser = class {
|
|
205
|
+
parseFromString(html, _type) {
|
|
206
|
+
let styles = [];
|
|
207
|
+
let lower = html.toLowerCase();
|
|
208
|
+
let pos = 0;
|
|
209
|
+
while (true) {
|
|
210
|
+
let open = lower.indexOf("<style", pos);
|
|
211
|
+
if (open === -1) break;
|
|
212
|
+
let start = lower.indexOf(">", open);
|
|
213
|
+
if (start === -1) break;
|
|
214
|
+
let close = lower.indexOf("</style>", start);
|
|
215
|
+
if (close === -1) break;
|
|
216
|
+
let text = html.slice(start + 1, close);
|
|
217
|
+
styles.push({ textContent: text });
|
|
218
|
+
pos = close + 8;
|
|
219
|
+
}
|
|
220
|
+
return { querySelectorAll(selector) {
|
|
221
|
+
return styles;
|
|
222
|
+
} };
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
197
226
|
//#endregion
|
|
198
227
|
//#region src/lib/remap-html.ts
|
|
199
|
-
|
|
228
|
+
function get_dom_parser() {
|
|
200
229
|
if (typeof window !== "undefined" && "DOMParser" in window) return new window.DOMParser();
|
|
201
|
-
let { DOMParser } = await import("./esm-CWr4VY0v.js");
|
|
202
230
|
return new DOMParser();
|
|
203
231
|
}
|
|
204
|
-
|
|
205
|
-
let doc =
|
|
232
|
+
function remap_html(html, old_ranges) {
|
|
233
|
+
let doc = get_dom_parser().parseFromString(html, "text/html");
|
|
206
234
|
let combined_css = "";
|
|
207
235
|
let new_ranges = [];
|
|
208
236
|
let current_offset = 0;
|
|
@@ -230,7 +258,20 @@ async function remap_html(html, old_ranges) {
|
|
|
230
258
|
function is_html(text) {
|
|
231
259
|
return /<\/?(html|body|head|div|span|script|style)/i.test(text);
|
|
232
260
|
}
|
|
233
|
-
|
|
261
|
+
const SELECTOR_REGEX = /(@[a-z-]+|\[[^\]]+\]|[a-zA-Z_#.-][a-zA-Z0-9_-]*)\s*\{/;
|
|
262
|
+
const DECLARATION_REGEX = /^\s*[a-zA-Z-]+\s*:\s*.+;?\s*$/m;
|
|
263
|
+
function is_css_like(text) {
|
|
264
|
+
return SELECTOR_REGEX.test(text) || DECLARATION_REGEX.test(text);
|
|
265
|
+
}
|
|
266
|
+
function is_js_like(text) {
|
|
267
|
+
try {
|
|
268
|
+
new Function(text);
|
|
269
|
+
return true;
|
|
270
|
+
} catch {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function filter_coverage(coverage) {
|
|
234
275
|
let result = [];
|
|
235
276
|
for (let entry of coverage) {
|
|
236
277
|
let extension = ext(entry.url).toLowerCase();
|
|
@@ -240,7 +281,7 @@ async function filter_coverage(coverage) {
|
|
|
240
281
|
continue;
|
|
241
282
|
}
|
|
242
283
|
if (is_html(entry.text)) {
|
|
243
|
-
let { css, ranges } =
|
|
284
|
+
let { css, ranges } = remap_html(entry.text, entry.ranges);
|
|
244
285
|
result.push({
|
|
245
286
|
url: entry.url,
|
|
246
287
|
text: css,
|
|
@@ -248,7 +289,7 @@ async function filter_coverage(coverage) {
|
|
|
248
289
|
});
|
|
249
290
|
continue;
|
|
250
291
|
}
|
|
251
|
-
result.push({
|
|
292
|
+
if (is_css_like(entry.text) && !is_js_like(entry.text)) result.push({
|
|
252
293
|
url: entry.url,
|
|
253
294
|
text: entry.text,
|
|
254
295
|
ranges: entry.ranges
|
|
@@ -347,9 +388,9 @@ function calculate_stylesheet_coverage({ text, url, chunks }) {
|
|
|
347
388
|
* 4. Calculate used/unused CSS bytes (fastest path, no inspection of the actual CSS needed)
|
|
348
389
|
* 5. Calculate line-coverage, byte-coverage per stylesheet
|
|
349
390
|
*/
|
|
350
|
-
|
|
391
|
+
function calculate_coverage(coverage) {
|
|
351
392
|
let total_files_found = coverage.length;
|
|
352
|
-
let coverage_per_stylesheet = extend_ranges(deduplicate_entries(
|
|
393
|
+
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));
|
|
353
394
|
let { total_lines, total_covered_lines, total_uncovered_lines, total_bytes, total_used_bytes, total_unused_bytes } = coverage_per_stylesheet.reduce((totals, sheet) => {
|
|
354
395
|
totals.total_lines += sheet.total_lines;
|
|
355
396
|
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.7.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": {
|
|
@@ -41,13 +41,14 @@
|
|
|
41
41
|
"build": "tsdown",
|
|
42
42
|
"check": "tsc --noEmit",
|
|
43
43
|
"lint": "oxlint --config .oxlintrc.json",
|
|
44
|
-
"lint-package": "publint"
|
|
44
|
+
"lint-package": "publint",
|
|
45
|
+
"knip": "knip"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
48
|
"@playwright/test": "^1.56.0",
|
|
48
|
-
"@types/node": "^24.
|
|
49
|
+
"@types/node": "^24.9.2",
|
|
49
50
|
"c8": "^10.1.3",
|
|
50
|
-
"
|
|
51
|
+
"knip": "^5.66.4",
|
|
51
52
|
"oxlint": "^1.22.0",
|
|
52
53
|
"publint": "^0.3.14",
|
|
53
54
|
"tsdown": "^0.15.8",
|