@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/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
|
|
|
@@ -194,15 +176,43 @@ function ext(url) {
|
|
|
194
176
|
}
|
|
195
177
|
}
|
|
196
178
|
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/lib/html-parser.ts
|
|
181
|
+
/**
|
|
182
|
+
* @description
|
|
183
|
+
* Very, very naive but effective DOMParser.
|
|
184
|
+
* It can only find <style> elements and their .textContent
|
|
185
|
+
*/
|
|
186
|
+
var DOMParser = class {
|
|
187
|
+
parseFromString(html, _type) {
|
|
188
|
+
let styles = [];
|
|
189
|
+
let lower = html.toLowerCase();
|
|
190
|
+
let pos = 0;
|
|
191
|
+
while (true) {
|
|
192
|
+
let open = lower.indexOf("<style", pos);
|
|
193
|
+
if (open === -1) break;
|
|
194
|
+
let start = lower.indexOf(">", open);
|
|
195
|
+
if (start === -1) break;
|
|
196
|
+
let close = lower.indexOf("</style>", start);
|
|
197
|
+
if (close === -1) break;
|
|
198
|
+
let text = html.slice(start + 1, close);
|
|
199
|
+
styles.push({ textContent: text });
|
|
200
|
+
pos = close + 8;
|
|
201
|
+
}
|
|
202
|
+
return { querySelectorAll(selector) {
|
|
203
|
+
return styles;
|
|
204
|
+
} };
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
197
208
|
//#endregion
|
|
198
209
|
//#region src/lib/remap-html.ts
|
|
199
|
-
|
|
210
|
+
function get_dom_parser() {
|
|
200
211
|
if (typeof window !== "undefined" && "DOMParser" in window) return new window.DOMParser();
|
|
201
|
-
let { DOMParser } = await import("./esm-CWr4VY0v.js");
|
|
202
212
|
return new DOMParser();
|
|
203
213
|
}
|
|
204
|
-
|
|
205
|
-
let doc =
|
|
214
|
+
function remap_html(html, old_ranges) {
|
|
215
|
+
let doc = get_dom_parser().parseFromString(html, "text/html");
|
|
206
216
|
let combined_css = "";
|
|
207
217
|
let new_ranges = [];
|
|
208
218
|
let current_offset = 0;
|
|
@@ -230,31 +240,44 @@ async function remap_html(html, old_ranges) {
|
|
|
230
240
|
function is_html(text) {
|
|
231
241
|
return /<\/?(html|body|head|div|span|script|style)/i.test(text);
|
|
232
242
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
243
|
+
const SELECTOR_REGEX = /(@[a-z-]+|\[[^\]]+\]|[a-zA-Z_#.-][a-zA-Z0-9_-]*)\s*\{/;
|
|
244
|
+
const DECLARATION_REGEX = /^\s*[a-zA-Z-]+\s*:\s*.+;?\s*$/m;
|
|
245
|
+
function is_css_like(text) {
|
|
246
|
+
return SELECTOR_REGEX.test(text) || DECLARATION_REGEX.test(text);
|
|
247
|
+
}
|
|
248
|
+
function is_js_like(text) {
|
|
249
|
+
try {
|
|
250
|
+
new Function(text);
|
|
251
|
+
return true;
|
|
252
|
+
} catch {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
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({
|
|
252
274
|
url: entry.url,
|
|
253
275
|
text: entry.text,
|
|
254
276
|
ranges: entry.ranges
|
|
255
277
|
});
|
|
278
|
+
return acc;
|
|
256
279
|
}
|
|
257
|
-
return
|
|
280
|
+
return acc;
|
|
258
281
|
}
|
|
259
282
|
|
|
260
283
|
//#endregion
|
|
@@ -262,38 +285,37 @@ async function filter_coverage(coverage) {
|
|
|
262
285
|
const AT_SIGN = 64;
|
|
263
286
|
const LONGEST_ATRULE_NAME = 28;
|
|
264
287
|
function extend_ranges(coverage) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
if (next_char$1 === "{") range.end = range.end + 1;
|
|
282
|
-
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);
|
|
283
303
|
}
|
|
304
|
+
if (next_char$1 === "{") range.end = range.end + 1;
|
|
305
|
+
break;
|
|
284
306
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
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
|
+
};
|
|
297
319
|
}
|
|
298
320
|
|
|
299
321
|
//#endregion
|
|
@@ -336,20 +358,9 @@ function calculate_stylesheet_coverage({ text, url, chunks }) {
|
|
|
336
358
|
chunks
|
|
337
359
|
};
|
|
338
360
|
}
|
|
339
|
-
|
|
340
|
-
* @description
|
|
341
|
-
* CSS Code Coverage calculation
|
|
342
|
-
*
|
|
343
|
-
* These are the steps performed to calculate coverage:
|
|
344
|
-
* 1. Filter eligible files / validate input
|
|
345
|
-
* 2. Prettify the CSS dicovered in each Coverage and update their ranges
|
|
346
|
-
* 3. De-duplicate Coverages: merge all ranges for CSS sources occurring multiple times
|
|
347
|
-
* 4. Calculate used/unused CSS bytes (fastest path, no inspection of the actual CSS needed)
|
|
348
|
-
* 5. Calculate line-coverage, byte-coverage per stylesheet
|
|
349
|
-
*/
|
|
350
|
-
async function calculate_coverage(coverage) {
|
|
361
|
+
function calculate_coverage(coverage) {
|
|
351
362
|
let total_files_found = coverage.length;
|
|
352
|
-
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));
|
|
353
364
|
let { total_lines, total_covered_lines, total_uncovered_lines, total_bytes, total_used_bytes, total_unused_bytes } = coverage_per_stylesheet.reduce((totals, sheet) => {
|
|
354
365
|
totals.total_lines += sheet.total_lines;
|
|
355
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.0",
|
|
4
4
|
"description": "Generate useful CSS Code Coverage report from browser-reported coverage",
|
|
5
5
|
"author": "Bart Veneman <bart@projectwallace.com>",
|
|
6
6
|
"repository": {
|
|
@@ -37,17 +37,19 @@
|
|
|
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",
|
|
44
|
-
"lint-package": "publint"
|
|
44
|
+
"lint-package": "publint",
|
|
45
|
+
"knip": "knip"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
48
|
+
"@codecov/vite-plugin": "^1.9.1",
|
|
47
49
|
"@playwright/test": "^1.56.0",
|
|
48
|
-
"@types/node": "^24.
|
|
50
|
+
"@types/node": "^24.9.2",
|
|
49
51
|
"c8": "^10.1.3",
|
|
50
|
-
"
|
|
52
|
+
"knip": "^5.66.4",
|
|
51
53
|
"oxlint": "^1.22.0",
|
|
52
54
|
"publint": "^0.3.14",
|
|
53
55
|
"tsdown": "^0.15.8",
|