@projectwallace/css-code-coverage 0.6.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/README.md +1 -1
- package/dist/cli.js +253 -168
- package/dist/index.d.ts +1 -12
- package/dist/index.js +135 -124
- package/package.json +7 -5
- 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
|
@@ -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
|
|
|
@@ -259,15 +241,43 @@ function ext(url) {
|
|
|
259
241
|
}
|
|
260
242
|
}
|
|
261
243
|
|
|
244
|
+
//#endregion
|
|
245
|
+
//#region src/lib/html-parser.ts
|
|
246
|
+
/**
|
|
247
|
+
* @description
|
|
248
|
+
* Very, very naive but effective DOMParser.
|
|
249
|
+
* It can only find <style> elements and their .textContent
|
|
250
|
+
*/
|
|
251
|
+
var DOMParser = class {
|
|
252
|
+
parseFromString(html, _type) {
|
|
253
|
+
let styles = [];
|
|
254
|
+
let lower = html.toLowerCase();
|
|
255
|
+
let pos = 0;
|
|
256
|
+
while (true) {
|
|
257
|
+
let open = lower.indexOf("<style", pos);
|
|
258
|
+
if (open === -1) break;
|
|
259
|
+
let start = lower.indexOf(">", open);
|
|
260
|
+
if (start === -1) break;
|
|
261
|
+
let close = lower.indexOf("</style>", start);
|
|
262
|
+
if (close === -1) break;
|
|
263
|
+
let text = html.slice(start + 1, close);
|
|
264
|
+
styles.push({ textContent: text });
|
|
265
|
+
pos = close + 8;
|
|
266
|
+
}
|
|
267
|
+
return { querySelectorAll(selector) {
|
|
268
|
+
return styles;
|
|
269
|
+
} };
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
262
273
|
//#endregion
|
|
263
274
|
//#region src/lib/remap-html.ts
|
|
264
|
-
|
|
275
|
+
function get_dom_parser() {
|
|
265
276
|
if (typeof window !== "undefined" && "DOMParser" in window) return new window.DOMParser();
|
|
266
|
-
let { DOMParser } = await import("./esm--VCpEgdH.js");
|
|
267
277
|
return new DOMParser();
|
|
268
278
|
}
|
|
269
|
-
|
|
270
|
-
let doc =
|
|
279
|
+
function remap_html(html, old_ranges) {
|
|
280
|
+
let doc = get_dom_parser().parseFromString(html, "text/html");
|
|
271
281
|
let combined_css = "";
|
|
272
282
|
let new_ranges = [];
|
|
273
283
|
let current_offset = 0;
|
|
@@ -295,31 +305,44 @@ async function remap_html(html, old_ranges) {
|
|
|
295
305
|
function is_html(text) {
|
|
296
306
|
return /<\/?(html|body|head|div|span|script|style)/i.test(text);
|
|
297
307
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
308
|
+
const SELECTOR_REGEX = /(@[a-z-]+|\[[^\]]+\]|[a-zA-Z_#.-][a-zA-Z0-9_-]*)\s*\{/;
|
|
309
|
+
const DECLARATION_REGEX = /^\s*[a-zA-Z-]+\s*:\s*.+;?\s*$/m;
|
|
310
|
+
function is_css_like(text) {
|
|
311
|
+
return SELECTOR_REGEX.test(text) || DECLARATION_REGEX.test(text);
|
|
312
|
+
}
|
|
313
|
+
function is_js_like(text) {
|
|
314
|
+
try {
|
|
315
|
+
new Function(text);
|
|
316
|
+
return true;
|
|
317
|
+
} catch {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
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({
|
|
317
339
|
url: entry.url,
|
|
318
340
|
text: entry.text,
|
|
319
341
|
ranges: entry.ranges
|
|
320
342
|
});
|
|
343
|
+
return acc;
|
|
321
344
|
}
|
|
322
|
-
return
|
|
345
|
+
return acc;
|
|
323
346
|
}
|
|
324
347
|
|
|
325
348
|
//#endregion
|
|
@@ -327,38 +350,37 @@ async function filter_coverage(coverage) {
|
|
|
327
350
|
const AT_SIGN = 64;
|
|
328
351
|
const LONGEST_ATRULE_NAME = 28;
|
|
329
352
|
function extend_ranges(coverage) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
if (next_char$1 === "{") range.end = range.end + 1;
|
|
347
|
-
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);
|
|
348
368
|
}
|
|
369
|
+
if (next_char$1 === "{") range.end = range.end + 1;
|
|
370
|
+
break;
|
|
349
371
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
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
|
+
};
|
|
362
384
|
}
|
|
363
385
|
|
|
364
386
|
//#endregion
|
|
@@ -401,20 +423,9 @@ function calculate_stylesheet_coverage({ text, url, chunks }) {
|
|
|
401
423
|
chunks
|
|
402
424
|
};
|
|
403
425
|
}
|
|
404
|
-
|
|
405
|
-
* @description
|
|
406
|
-
* CSS Code Coverage calculation
|
|
407
|
-
*
|
|
408
|
-
* These are the steps performed to calculate coverage:
|
|
409
|
-
* 1. Filter eligible files / validate input
|
|
410
|
-
* 2. Prettify the CSS dicovered in each Coverage and update their ranges
|
|
411
|
-
* 3. De-duplicate Coverages: merge all ranges for CSS sources occurring multiple times
|
|
412
|
-
* 4. Calculate used/unused CSS bytes (fastest path, no inspection of the actual CSS needed)
|
|
413
|
-
* 5. Calculate line-coverage, byte-coverage per stylesheet
|
|
414
|
-
*/
|
|
415
|
-
async function calculate_coverage(coverage) {
|
|
426
|
+
function calculate_coverage(coverage) {
|
|
416
427
|
let total_files_found = coverage.length;
|
|
417
|
-
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));
|
|
418
429
|
let { total_lines, total_covered_lines, total_uncovered_lines, total_bytes, total_used_bytes, total_unused_bytes } = coverage_per_stylesheet.reduce((totals, sheet) => {
|
|
419
430
|
totals.total_lines += sheet.total_lines;
|
|
420
431
|
totals.total_covered_lines += sheet.covered_lines;
|
|
@@ -448,11 +459,6 @@ async function calculate_coverage(coverage) {
|
|
|
448
459
|
|
|
449
460
|
//#endregion
|
|
450
461
|
//#region src/cli/program.ts
|
|
451
|
-
var MissingDataError = class extends Error {
|
|
452
|
-
constructor() {
|
|
453
|
-
super("No data to analyze");
|
|
454
|
-
}
|
|
455
|
-
};
|
|
456
462
|
function validate_min_line_coverage(actual, expected) {
|
|
457
463
|
return {
|
|
458
464
|
ok: actual >= expected,
|
|
@@ -472,17 +478,16 @@ function validate_min_file_line_coverage(actual, expected) {
|
|
|
472
478
|
expected
|
|
473
479
|
};
|
|
474
480
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
let
|
|
478
|
-
let
|
|
479
|
-
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);
|
|
481
|
+
function program({ min_coverage, min_file_coverage }, coverage_data) {
|
|
482
|
+
let coverage = calculate_coverage(coverage_data);
|
|
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);
|
|
480
485
|
return {
|
|
481
486
|
context: { coverage },
|
|
482
487
|
report: {
|
|
483
|
-
ok:
|
|
484
|
-
min_line_coverage:
|
|
485
|
-
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
|
|
486
491
|
}
|
|
487
492
|
};
|
|
488
493
|
}
|
|
@@ -506,43 +511,61 @@ async function read(coverage_dir) {
|
|
|
506
511
|
function indent(line) {
|
|
507
512
|
return (line || "").replace(/^\t+/, (tabs) => " ".repeat(tabs.length * 4));
|
|
508
513
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
514
|
+
let line_number = (num, covered = true) => `${num.toString().padStart(5, " ")} ${covered ? "│" : "━"} `;
|
|
515
|
+
function percentage(ratio$1, decimals = 2) {
|
|
516
|
+
return `${(ratio$1 * 100).toFixed(ratio$1 === 1 ? 0 : decimals)}%`;
|
|
517
|
+
}
|
|
518
|
+
function print_lines({ report, context }, params, { styleText: styleText$1, print_width }) {
|
|
519
|
+
let output = [];
|
|
520
|
+
if (report.min_line_coverage.ok) output.push(`${styleText$1(["bold", "green"], "Success")}: total line coverage is ${percentage(report.min_line_coverage.actual)}`);
|
|
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
|
+
}
|
|
512
527
|
if (report.min_file_line_coverage.expected !== void 0) {
|
|
513
528
|
let { expected, actual, ok: ok$1 } = report.min_file_line_coverage;
|
|
514
|
-
if (ok$1)
|
|
529
|
+
if (ok$1) output.push(`${styleText$1(["bold", "green"], "Success")}: all files pass minimum line coverage of ${percentage(expected)}`);
|
|
515
530
|
else {
|
|
516
531
|
let num_files_failed = context.coverage.coverage_per_stylesheet.filter((sheet) => sheet.line_coverage_ratio < expected).length;
|
|
517
|
-
|
|
518
|
-
if (params["show-uncovered"] === "none")
|
|
532
|
+
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)})`);
|
|
533
|
+
if (params["show-uncovered"] === "none") output.push(` Hint: set --show-uncovered=violations to see which files didn't pass`);
|
|
519
534
|
}
|
|
520
535
|
}
|
|
521
536
|
if (params["show-uncovered"] !== "none") {
|
|
522
537
|
const NUM_LEADING_LINES = 3;
|
|
523
538
|
const NUM_TRAILING_LINES = NUM_LEADING_LINES;
|
|
524
|
-
|
|
525
|
-
let line_number = (num, covered = true) => `${num.toString().padStart(5, " ")} ${covered ? "│" : "━"} `;
|
|
539
|
+
print_width = print_width ?? 80;
|
|
526
540
|
let min_file_line_coverage = report.min_file_line_coverage.expected;
|
|
541
|
+
output.push();
|
|
527
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") {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
543
|
+
output.push();
|
|
544
|
+
output.push(styleText$1("dim", "─".repeat(print_width)));
|
|
545
|
+
output.push(sheet.url);
|
|
546
|
+
output.push(`Coverage: ${percentage(sheet.line_coverage_ratio)}, ${sheet.covered_lines}/${sheet.total_lines} lines covered`);
|
|
532
547
|
if (min_file_line_coverage && min_file_line_coverage !== 0 && sheet.line_coverage_ratio < min_file_line_coverage) {
|
|
533
548
|
let lines_to_cover = min_file_line_coverage * sheet.total_lines - sheet.covered_lines;
|
|
534
|
-
|
|
549
|
+
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
550
|
}
|
|
536
|
-
|
|
551
|
+
output.push(styleText$1("dim", "─".repeat(print_width)));
|
|
537
552
|
let lines = sheet.text.split("\n");
|
|
538
553
|
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
|
-
|
|
554
|
+
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(""));
|
|
555
|
+
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(""));
|
|
556
|
+
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(""));
|
|
557
|
+
output.push();
|
|
543
558
|
}
|
|
544
559
|
}
|
|
545
560
|
}
|
|
561
|
+
return output;
|
|
562
|
+
}
|
|
563
|
+
function print(report, params) {
|
|
564
|
+
let logger = report.report.ok ? console.log : console.error;
|
|
565
|
+
for (let line of print_lines(report, params, {
|
|
566
|
+
styleText,
|
|
567
|
+
print_width: process.stdout.columns
|
|
568
|
+
})) logger(line);
|
|
546
569
|
}
|
|
547
570
|
|
|
548
571
|
//#endregion
|
|
@@ -557,7 +580,7 @@ function ok(n, description) {
|
|
|
557
580
|
console.log(`ok ${n} ${description ? `- ${description}` : ""}`);
|
|
558
581
|
}
|
|
559
582
|
function not_ok(n, description) {
|
|
560
|
-
console.
|
|
583
|
+
console.error(`not ok ${n} ${description ? `- ${description}` : ""}`);
|
|
561
584
|
}
|
|
562
585
|
function meta(data) {
|
|
563
586
|
console.log(" ---");
|
|
@@ -601,18 +624,80 @@ function print$1({ report, context }, params) {
|
|
|
601
624
|
}
|
|
602
625
|
}
|
|
603
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
|
+
|
|
604
687
|
//#endregion
|
|
605
688
|
//#region src/cli/cli.ts
|
|
606
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());
|
|
607
691
|
let params = validate_arguments(parse_arguments(cli_args));
|
|
608
692
|
let coverage_data = await read(params["coverage-dir"]);
|
|
609
|
-
let report =
|
|
610
|
-
|
|
611
|
-
|
|
693
|
+
let report = program({
|
|
694
|
+
min_coverage: params["min-coverage"],
|
|
695
|
+
min_file_coverage: params["min-file-coverage"]
|
|
612
696
|
}, coverage_data);
|
|
613
697
|
if (report.report.ok === false) process.exitCode = 1;
|
|
614
|
-
if (params.reporter === "
|
|
615
|
-
|
|
698
|
+
if (params.reporter === "tap") return print$1(report, params);
|
|
699
|
+
if (params.reporter === "json") return print$2(report, params);
|
|
700
|
+
return print(report, params);
|
|
616
701
|
}
|
|
617
702
|
try {
|
|
618
703
|
await cli(process.argv.slice(2));
|
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
|
-
declare function calculate_coverage(coverage: Coverage[]): Promise<CoverageResult>;
|
|
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 };
|