@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 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 = await calculcate_coverage(coverage)
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
- async function get_dom_parser() {
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
- async function remap_html(html, old_ranges) {
270
- 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");
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
- 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) {
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 } = await remap_html(entry.text, entry.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
- async function calculate_coverage(coverage) {
456
+ function calculate_coverage(coverage) {
416
457
  let total_files_found = coverage.length;
417
- let coverage_per_stylesheet = extend_ranges(deduplicate_entries(await filter_coverage(coverage))).map((sheet) => chunkify(sheet)).map((sheet) => prettify(sheet)).map((stylesheet) => calculate_stylesheet_coverage(stylesheet));
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
- async function program({ min_file_coverage, min_file_line_coverage }, coverage_data) {
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 = await calculate_coverage(coverage_data);
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
- function print({ report, context }, params) {
510
- if (report.min_line_coverage.ok) console.log(`${styleText(["bold", "green"], "Success")}: total line coverage is ${(report.min_line_coverage.actual * 100).toFixed(2)}%`);
511
- 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}`);
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) 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)}`);
515
561
  else {
516
562
  let num_files_failed = context.coverage.coverage_per_stylesheet.filter((sheet) => sheet.line_coverage_ratio < expected).length;
517
- 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)}%)`);
518
- 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`);
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
- let terminal_width = process.stdout.columns || 80;
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
- console.log();
529
- console.log(styleText("dim", "─".repeat(terminal_width)));
530
- console.log(sheet.url);
531
- 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`);
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
- 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)}`);
535
580
  }
536
- console.log(styleText("dim", "─".repeat(terminal_width)));
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, 0); x < chunk.start_line; x++) console.log(styleText("dim", line_number(x)), styleText("dim", indent(lines[x - 1])));
540
- for (let i = chunk.start_line; i <= chunk.end_line; i++) console.log(styleText("red", line_number(i, false)), indent(lines[i - 1]));
541
- for (let y = chunk.end_line; y < Math.min(chunk.end_line + NUM_TRAILING_LINES, lines.length); y++) console.log(styleText("dim", line_number(y)), styleText("dim", indent(lines[y - 1])));
542
- console.log();
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 = await program({
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 === "pretty") print(report, params);
615
- else if (params.reporter === "tap") print$1(report, params);
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[]): Promise<CoverageResult>;
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
- async function get_dom_parser() {
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
- async function remap_html(html, old_ranges) {
205
- let doc = (await get_dom_parser()).parseFromString(html, "text/html");
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
- async function filter_coverage(coverage) {
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 } = await remap_html(entry.text, entry.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
- async function calculate_coverage(coverage) {
391
+ function calculate_coverage(coverage) {
351
392
  let total_files_found = coverage.length;
352
- let coverage_per_stylesheet = extend_ranges(deduplicate_entries(await filter_coverage(coverage))).map((sheet) => chunkify(sheet)).map((sheet) => prettify(sheet)).map((stylesheet) => calculate_stylesheet_coverage(stylesheet));
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.6.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.8.1",
49
+ "@types/node": "^24.9.2",
49
50
  "c8": "^10.1.3",
50
- "linkedom": "^0.18.12",
51
+ "knip": "^5.66.4",
51
52
  "oxlint": "^1.22.0",
52
53
  "publint": "^0.3.14",
53
54
  "tsdown": "^0.15.8",