@projectwallace/css-code-coverage 0.7.0 → 0.8.2
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 +10 -0
- package/dist/cli.js +112 -448
- package/dist/index.d.ts +0 -11
- package/dist/index.js +89 -119
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -87,3 +87,13 @@ for (let file of files) {
|
|
|
87
87
|
coverage_data.push(...parse_coverage(json_content))
|
|
88
88
|
}
|
|
89
89
|
```
|
|
90
|
+
|
|
91
|
+
## CLI
|
|
92
|
+
|
|
93
|
+
Use the CLI tool (`css-coverage`) to check if coverage meets minimum requirements, globally and/or per file.
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
css-coverage --coverage-dir=<dir> --min-coverage=<number> [options]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
[CLI docs](src/cli/README.md)
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs, styleText } from "node:util";
|
|
3
3
|
import * as v from "valibot";
|
|
4
|
-
import {
|
|
4
|
+
import { calculate_coverage } from "@projectwallace/css-code-coverage";
|
|
5
5
|
import { readFile, readdir, stat } from "node:fs/promises";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
|
|
@@ -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
|
},
|
|
@@ -65,435 +65,8 @@ function parse_arguments(args) {
|
|
|
65
65
|
return values;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
//#endregion
|
|
69
|
-
//#region src/lib/parse-coverage.ts
|
|
70
|
-
let RangeSchema = v.object({
|
|
71
|
-
start: v.number(),
|
|
72
|
-
end: v.number()
|
|
73
|
-
});
|
|
74
|
-
let CoverageSchema = v.object({
|
|
75
|
-
text: v.string(),
|
|
76
|
-
url: v.string(),
|
|
77
|
-
ranges: v.array(RangeSchema)
|
|
78
|
-
});
|
|
79
|
-
function is_valid_coverage(input) {
|
|
80
|
-
return v.safeParse(v.array(CoverageSchema), input).success;
|
|
81
|
-
}
|
|
82
|
-
function parse_coverage(input) {
|
|
83
|
-
try {
|
|
84
|
-
let parse_result = JSON.parse(input);
|
|
85
|
-
return is_valid_coverage(parse_result) ? parse_result : [];
|
|
86
|
-
} catch {
|
|
87
|
-
return [];
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
//#endregion
|
|
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;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
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
|
|
127
|
-
});
|
|
128
|
-
offset = range.start;
|
|
129
|
-
}
|
|
130
|
-
chunks.push({
|
|
131
|
-
start_offset: range.start,
|
|
132
|
-
end_offset: range.end,
|
|
133
|
-
is_covered: true
|
|
134
|
-
});
|
|
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";
|
|
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;
|
|
169
|
-
return {
|
|
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
|
|
177
|
-
};
|
|
178
|
-
});
|
|
179
|
-
return {
|
|
180
|
-
...stylesheet,
|
|
181
|
-
chunks: pretty_chunks,
|
|
182
|
-
text: pretty_chunks.map(({ css }) => css).join("\n")
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
//#endregion
|
|
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
|
-
}
|
|
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
|
-
function deduplicate_entries(entries) {
|
|
225
|
-
let checked_stylesheets = /* @__PURE__ */ new Map();
|
|
226
|
-
for (let entry of entries) {
|
|
227
|
-
let text = entry.text;
|
|
228
|
-
if (checked_stylesheets.has(text)) {
|
|
229
|
-
let ranges = checked_stylesheets.get(text).ranges;
|
|
230
|
-
for (let range of entry.ranges) {
|
|
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 }]) => ({
|
|
244
|
-
text,
|
|
245
|
-
url,
|
|
246
|
-
ranges: concatenate(dedupe_list(ranges.sort((a, b) => a.start - b.start))).sort((a, b) => a.start - b.start)
|
|
247
|
-
}));
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
//#endregion
|
|
251
|
-
//#region src/lib/ext.ts
|
|
252
|
-
function ext(url) {
|
|
253
|
-
try {
|
|
254
|
-
let parsed_url = new URL(url);
|
|
255
|
-
return parsed_url.pathname.slice(parsed_url.pathname.lastIndexOf(".") + 1);
|
|
256
|
-
} catch {
|
|
257
|
-
let ext_index = url.lastIndexOf(".");
|
|
258
|
-
return url.slice(ext_index, url.indexOf("/", ext_index) + 1);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
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
|
-
|
|
291
|
-
//#endregion
|
|
292
|
-
//#region src/lib/remap-html.ts
|
|
293
|
-
function get_dom_parser() {
|
|
294
|
-
if (typeof window !== "undefined" && "DOMParser" in window) return new window.DOMParser();
|
|
295
|
-
return new DOMParser();
|
|
296
|
-
}
|
|
297
|
-
function remap_html(html, old_ranges) {
|
|
298
|
-
let doc = get_dom_parser().parseFromString(html, "text/html");
|
|
299
|
-
let combined_css = "";
|
|
300
|
-
let new_ranges = [];
|
|
301
|
-
let current_offset = 0;
|
|
302
|
-
let style_elements = doc.querySelectorAll("style");
|
|
303
|
-
for (let style_element of Array.from(style_elements)) {
|
|
304
|
-
let style_content = style_element.textContent;
|
|
305
|
-
if (!style_content.trim()) continue;
|
|
306
|
-
combined_css += style_content;
|
|
307
|
-
let start_index = html.indexOf(style_content);
|
|
308
|
-
let end_index = start_index + style_content.length;
|
|
309
|
-
for (let range of old_ranges) if (range.start >= start_index && range.end <= end_index) new_ranges.push({
|
|
310
|
-
start: current_offset + (range.start - start_index),
|
|
311
|
-
end: current_offset + (range.end - start_index)
|
|
312
|
-
});
|
|
313
|
-
current_offset += style_content.length;
|
|
314
|
-
}
|
|
315
|
-
return {
|
|
316
|
-
css: combined_css,
|
|
317
|
-
ranges: new_ranges
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
//#endregion
|
|
322
|
-
//#region src/lib/filter-entries.ts
|
|
323
|
-
function is_html(text) {
|
|
324
|
-
return /<\/?(html|body|head|div|span|script|style)/i.test(text);
|
|
325
|
-
}
|
|
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) {
|
|
340
|
-
let result = [];
|
|
341
|
-
for (let entry of coverage) {
|
|
342
|
-
let extension = ext(entry.url).toLowerCase();
|
|
343
|
-
if (extension === "js") continue;
|
|
344
|
-
if (extension === "css") {
|
|
345
|
-
result.push(entry);
|
|
346
|
-
continue;
|
|
347
|
-
}
|
|
348
|
-
if (is_html(entry.text)) {
|
|
349
|
-
let { css, ranges } = remap_html(entry.text, entry.ranges);
|
|
350
|
-
result.push({
|
|
351
|
-
url: entry.url,
|
|
352
|
-
text: css,
|
|
353
|
-
ranges
|
|
354
|
-
});
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
if (is_css_like(entry.text) && !is_js_like(entry.text)) result.push({
|
|
358
|
-
url: entry.url,
|
|
359
|
-
text: entry.text,
|
|
360
|
-
ranges: entry.ranges
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
return result;
|
|
364
|
-
}
|
|
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
|
-
|
|
405
|
-
//#endregion
|
|
406
|
-
//#region src/lib/index.ts
|
|
407
|
-
function ratio(fraction, total) {
|
|
408
|
-
if (total === 0) return 0;
|
|
409
|
-
return fraction / total;
|
|
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
|
-
}
|
|
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
|
-
function calculate_coverage(coverage) {
|
|
457
|
-
let total_files_found = coverage.length;
|
|
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));
|
|
459
|
-
let { total_lines, total_covered_lines, total_uncovered_lines, total_bytes, total_used_bytes, total_unused_bytes } = coverage_per_stylesheet.reduce((totals, sheet) => {
|
|
460
|
-
totals.total_lines += sheet.total_lines;
|
|
461
|
-
totals.total_covered_lines += sheet.covered_lines;
|
|
462
|
-
totals.total_uncovered_lines += sheet.uncovered_lines;
|
|
463
|
-
totals.total_bytes += sheet.total_bytes;
|
|
464
|
-
totals.total_used_bytes += sheet.covered_bytes;
|
|
465
|
-
totals.total_unused_bytes += sheet.uncovered_bytes;
|
|
466
|
-
return totals;
|
|
467
|
-
}, {
|
|
468
|
-
total_lines: 0,
|
|
469
|
-
total_covered_lines: 0,
|
|
470
|
-
total_uncovered_lines: 0,
|
|
471
|
-
total_bytes: 0,
|
|
472
|
-
total_used_bytes: 0,
|
|
473
|
-
total_unused_bytes: 0
|
|
474
|
-
});
|
|
475
|
-
return {
|
|
476
|
-
total_files_found,
|
|
477
|
-
total_bytes,
|
|
478
|
-
total_lines,
|
|
479
|
-
covered_bytes: total_used_bytes,
|
|
480
|
-
covered_lines: total_covered_lines,
|
|
481
|
-
uncovered_bytes: total_unused_bytes,
|
|
482
|
-
uncovered_lines: total_uncovered_lines,
|
|
483
|
-
byte_coverage_ratio: ratio(total_used_bytes, total_bytes),
|
|
484
|
-
line_coverage_ratio: ratio(total_covered_lines, total_lines),
|
|
485
|
-
coverage_per_stylesheet,
|
|
486
|
-
total_stylesheets: coverage_per_stylesheet.length
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
|
|
490
68
|
//#endregion
|
|
491
69
|
//#region src/cli/program.ts
|
|
492
|
-
var MissingDataError = class extends Error {
|
|
493
|
-
constructor() {
|
|
494
|
-
super("No data to analyze");
|
|
495
|
-
}
|
|
496
|
-
};
|
|
497
70
|
function validate_min_line_coverage(actual, expected) {
|
|
498
71
|
return {
|
|
499
72
|
ok: actual >= expected,
|
|
@@ -513,21 +86,43 @@ function validate_min_file_line_coverage(actual, expected) {
|
|
|
513
86
|
expected
|
|
514
87
|
};
|
|
515
88
|
}
|
|
516
|
-
function program({
|
|
517
|
-
if (coverage_data.length === 0) throw new MissingDataError();
|
|
89
|
+
function program({ min_coverage, min_file_coverage }, coverage_data) {
|
|
518
90
|
let coverage = calculate_coverage(coverage_data);
|
|
519
|
-
let
|
|
520
|
-
let
|
|
91
|
+
let min_coverage_result = validate_min_line_coverage(coverage.line_coverage_ratio, min_coverage);
|
|
92
|
+
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
93
|
return {
|
|
522
94
|
context: { coverage },
|
|
523
95
|
report: {
|
|
524
|
-
ok:
|
|
525
|
-
min_line_coverage:
|
|
526
|
-
min_file_line_coverage:
|
|
96
|
+
ok: min_coverage_result.ok && min_file_coverage_result.ok,
|
|
97
|
+
min_line_coverage: min_coverage_result,
|
|
98
|
+
min_file_line_coverage: min_file_coverage_result
|
|
527
99
|
}
|
|
528
100
|
};
|
|
529
101
|
}
|
|
530
102
|
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/lib/parse-coverage.ts
|
|
105
|
+
let RangeSchema = v.object({
|
|
106
|
+
start: v.number(),
|
|
107
|
+
end: v.number()
|
|
108
|
+
});
|
|
109
|
+
let CoverageSchema = v.object({
|
|
110
|
+
text: v.string(),
|
|
111
|
+
url: v.string(),
|
|
112
|
+
ranges: v.array(RangeSchema)
|
|
113
|
+
});
|
|
114
|
+
function is_valid_coverage(input) {
|
|
115
|
+
return v.safeParse(v.array(CoverageSchema), input).success;
|
|
116
|
+
}
|
|
117
|
+
function parse_coverage(input) {
|
|
118
|
+
try {
|
|
119
|
+
let parse_result = JSON.parse(input);
|
|
120
|
+
return is_valid_coverage(parse_result) ? parse_result : [];
|
|
121
|
+
} catch {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
531
126
|
//#endregion
|
|
532
127
|
//#region src/cli/file-reader.ts
|
|
533
128
|
async function read(coverage_dir) {
|
|
@@ -548,13 +143,18 @@ function indent(line) {
|
|
|
548
143
|
return (line || "").replace(/^\t+/, (tabs) => " ".repeat(tabs.length * 4));
|
|
549
144
|
}
|
|
550
145
|
let line_number = (num, covered = true) => `${num.toString().padStart(5, " ")} ${covered ? "│" : "━"} `;
|
|
551
|
-
function percentage(ratio
|
|
552
|
-
return `${(ratio
|
|
146
|
+
function percentage(ratio, decimals = 2) {
|
|
147
|
+
return `${(ratio * 100).toFixed(ratio === 1 ? 0 : decimals)}%`;
|
|
553
148
|
}
|
|
554
149
|
function print_lines({ report, context }, params, { styleText: styleText$1, print_width }) {
|
|
555
150
|
let output = [];
|
|
556
151
|
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
|
|
152
|
+
else {
|
|
153
|
+
let { actual, expected } = report.min_line_coverage;
|
|
154
|
+
output.push(`${styleText$1(["bold", "red"], "Failed")}: line coverage is ${percentage(actual)}% which is lower than the threshold of ${expected}`);
|
|
155
|
+
let lines_to_cover = expected * context.coverage.total_lines - context.coverage.covered_lines;
|
|
156
|
+
output.push(`Tip: cover ${Math.ceil(lines_to_cover)} more ${lines_to_cover === 1 ? "line" : "lines"} to meet the threshold of ${percentage(expected)}`);
|
|
157
|
+
}
|
|
558
158
|
if (report.min_file_line_coverage.expected !== void 0) {
|
|
559
159
|
let { expected, actual, ok: ok$1 } = report.min_file_line_coverage;
|
|
560
160
|
if (ok$1) output.push(`${styleText$1(["bold", "green"], "Success")}: all files pass minimum line coverage of ${percentage(expected)}`);
|
|
@@ -569,6 +169,7 @@ function print_lines({ report, context }, params, { styleText: styleText$1, prin
|
|
|
569
169
|
const NUM_TRAILING_LINES = NUM_LEADING_LINES;
|
|
570
170
|
print_width = print_width ?? 80;
|
|
571
171
|
let min_file_line_coverage = report.min_file_line_coverage.expected;
|
|
172
|
+
output.push();
|
|
572
173
|
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
174
|
output.push();
|
|
574
175
|
output.push(styleText$1("dim", "─".repeat(print_width)));
|
|
@@ -591,10 +192,11 @@ function print_lines({ report, context }, params, { styleText: styleText$1, prin
|
|
|
591
192
|
return output;
|
|
592
193
|
}
|
|
593
194
|
function print(report, params) {
|
|
195
|
+
let logger = report.report.ok ? console.log : console.error;
|
|
594
196
|
for (let line of print_lines(report, params, {
|
|
595
197
|
styleText,
|
|
596
198
|
print_width: process.stdout.columns
|
|
597
|
-
}))
|
|
199
|
+
})) logger(line);
|
|
598
200
|
}
|
|
599
201
|
|
|
600
202
|
//#endregion
|
|
@@ -609,7 +211,7 @@ function ok(n, description) {
|
|
|
609
211
|
console.log(`ok ${n} ${description ? `- ${description}` : ""}`);
|
|
610
212
|
}
|
|
611
213
|
function not_ok(n, description) {
|
|
612
|
-
console.
|
|
214
|
+
console.error(`not ok ${n} ${description ? `- ${description}` : ""}`);
|
|
613
215
|
}
|
|
614
216
|
function meta(data) {
|
|
615
217
|
console.log(" ---");
|
|
@@ -653,17 +255,79 @@ function print$1({ report, context }, params) {
|
|
|
653
255
|
}
|
|
654
256
|
}
|
|
655
257
|
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region src/cli/reporters/json.ts
|
|
260
|
+
function prepare({ report, context }, params) {
|
|
261
|
+
context.coverage.coverage_per_stylesheet = context.coverage.coverage_per_stylesheet.filter((sheet) => {
|
|
262
|
+
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;
|
|
263
|
+
if (params["show-uncovered"] === "all" && sheet.line_coverage_ratio < 1) return true;
|
|
264
|
+
return false;
|
|
265
|
+
});
|
|
266
|
+
return {
|
|
267
|
+
report,
|
|
268
|
+
context
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function print$2({ report, context }, params) {
|
|
272
|
+
let logger = report.ok ? console.log : console.error;
|
|
273
|
+
let data = prepare({
|
|
274
|
+
context,
|
|
275
|
+
report
|
|
276
|
+
}, params);
|
|
277
|
+
logger(JSON.stringify(data));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region src/cli/help.ts
|
|
282
|
+
function help() {
|
|
283
|
+
return `
|
|
284
|
+
${styleText(["bold"], "USAGE")}
|
|
285
|
+
$ css-coverage --coverage-dir=<dir> --min-coverage=<number> [options]
|
|
286
|
+
|
|
287
|
+
${styleText("bold", "OPTIONS")}
|
|
288
|
+
Required:
|
|
289
|
+
--coverage-dir Where your Coverage JSON files are
|
|
290
|
+
--min-coverage Minimum overall CSS coverage [0-1]
|
|
291
|
+
|
|
292
|
+
Optional:
|
|
293
|
+
--min-file-coverage Minimal coverage per file [0-1]
|
|
294
|
+
|
|
295
|
+
--show-uncovered Which files to show when not meeting
|
|
296
|
+
the --min-file-line-coverage threshold
|
|
297
|
+
• violations [default] ${styleText("dim", "show under-threshold files")}
|
|
298
|
+
• all ${styleText("dim", "show partially covered files")}
|
|
299
|
+
• none ${styleText("dim", "do not show files")}
|
|
300
|
+
|
|
301
|
+
--reporter How to show the results
|
|
302
|
+
• pretty [default]
|
|
303
|
+
• tap
|
|
304
|
+
• json
|
|
305
|
+
|
|
306
|
+
${styleText("bold", "EXAMPLES")}
|
|
307
|
+
${styleText("dim", "# analyze all .json files in ./coverage; require 80% overall coverage")}
|
|
308
|
+
$ css-coverage --coverage-dir=./coverage --min-coverage=0.8
|
|
309
|
+
|
|
310
|
+
${styleText("dim", "# Require 50% coverage per file")}
|
|
311
|
+
$ css-coverage --coverage-dir=./coverage --min-coverage=0.8 --min-file-coverage=0.5
|
|
312
|
+
|
|
313
|
+
${styleText("dim", "Report JSON")}
|
|
314
|
+
$ css-coverage --coverage-dir=./coverage --min-coverage=0.8 --reporter=json
|
|
315
|
+
`.trim();
|
|
316
|
+
}
|
|
317
|
+
|
|
656
318
|
//#endregion
|
|
657
319
|
//#region src/cli/cli.ts
|
|
658
320
|
async function cli(cli_args) {
|
|
321
|
+
if (!cli_args || cli_args.length === 0 || cli_args.includes("--help") || cli_args.includes("-h")) return console.log(help());
|
|
659
322
|
let params = validate_arguments(parse_arguments(cli_args));
|
|
660
323
|
let coverage_data = await read(params["coverage-dir"]);
|
|
661
324
|
let report = program({
|
|
662
|
-
|
|
663
|
-
|
|
325
|
+
min_coverage: params["min-coverage"],
|
|
326
|
+
min_file_coverage: params["min-file-coverage"]
|
|
664
327
|
}, coverage_data);
|
|
665
328
|
if (report.report.ok === false) process.exitCode = 1;
|
|
666
329
|
if (params.reporter === "tap") return print$1(report, params);
|
|
330
|
+
if (params.reporter === "json") return print$2(report, params);
|
|
667
331
|
return print(report, params);
|
|
668
332
|
}
|
|
669
333
|
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.2",
|
|
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",
|