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