@projectwallace/css-code-coverage 0.1.3 → 0.2.1
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 +44 -11
- package/dist/css-code-coverage.js +164 -143
- package/dist/src/index.d.ts +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# CSS Code Coverage
|
|
2
2
|
|
|
3
|
+
> [!WARNING]
|
|
4
|
+
> This is a very experimental approach to calculating CSS Code Coverage and currently very much a work in progress.
|
|
5
|
+
|
|
3
6
|
Takes your generated coverage files and turns them into something actually usable. Accepts coverage reports generated by browsers (Edge/Chrome/chromium), Puppeteer, Playwright.
|
|
4
7
|
|
|
5
8
|
Features include:
|
|
@@ -17,14 +20,32 @@ npm install @projectwallace/css-code-coverage
|
|
|
17
20
|
|
|
18
21
|
## Usage
|
|
19
22
|
|
|
20
|
-
|
|
23
|
+
```ts
|
|
24
|
+
import { calculate_coverage } from '@projectwallace/css-code-coverage'
|
|
25
|
+
|
|
26
|
+
function parse_html(html) {
|
|
27
|
+
return new DOMParser().parseFromString(html, 'text/html')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let report = calculcate_coverage(coverage_data, parse_html)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
See [src/index.ts](https://github.com/projectwallace/css-code-coverage/blob/main/src/index.ts) for the data that's returned.
|
|
34
|
+
|
|
35
|
+
## Collecting CSS Coverage
|
|
36
|
+
|
|
37
|
+
There are two principal ways of collecting CSS Coverage data:
|
|
38
|
+
|
|
39
|
+
### Browser devtools
|
|
21
40
|
|
|
22
|
-
|
|
41
|
+
In Edge, Chrome or chromium you can manually collect coverage in the browser's DevTools. In all cases you'll generate coverage data manually and the browser will let you export the data to a JSON file. Note that this JSON contains both JS coverage as well as the CSS coverage. Learn how it works:
|
|
23
42
|
|
|
24
|
-
|
|
25
|
-
|
|
43
|
+
- Collect coverage in Microsoft Edge: https://learn.microsoft.com/en-us/microsoft-edge/devtools-guide-chromium/coverage/
|
|
44
|
+
- Collect coevrage in Google Chrome: https://developer.chrome.com/docs/devtools/coverage/
|
|
26
45
|
|
|
27
|
-
|
|
46
|
+
Additionally, DevTools Tips writes about it in their [explainer](https://devtoolstips.org/tips/en/detect-unused-code/).
|
|
47
|
+
|
|
48
|
+
You end up with one or more JSON files that contain coverage data. We provide a helper `parse_coverage()` that both parses the JSON and validates it so you can pass it directly into `calculate_coverage()`.
|
|
28
49
|
|
|
29
50
|
```ts
|
|
30
51
|
// Read a single JSON or a folder full of JSON files with coverage data
|
|
@@ -47,19 +68,31 @@ for (let file of files) {
|
|
|
47
68
|
}
|
|
48
69
|
```
|
|
49
70
|
|
|
50
|
-
###
|
|
71
|
+
### Coverage API
|
|
72
|
+
|
|
73
|
+
Both Puppeteer and Playwright provide an API to programmatically get the coverage data, allowing you to put that directly into this library. Here is the gist:
|
|
51
74
|
|
|
52
75
|
```ts
|
|
76
|
+
// Start collecting coverage
|
|
77
|
+
await page.coverage.startCSSCoverage()
|
|
78
|
+
// Load the page, do all sorts of interactions to increase coverage, etc.
|
|
79
|
+
await page.goto('http://example.com')
|
|
80
|
+
// Stop the coverage and store the result in a variable to pass along
|
|
81
|
+
let coverage = await page.coverage.stopCSSCoverage()
|
|
82
|
+
|
|
83
|
+
// Now we can process it
|
|
53
84
|
import { calculate_coverage } from '@projectwallace/css-code-coverage'
|
|
54
85
|
|
|
55
|
-
|
|
56
|
-
|
|
86
|
+
function parse_html(html) {
|
|
87
|
+
return new DOMParser().parseFromString(html, 'text/html')
|
|
88
|
+
}
|
|
57
89
|
|
|
58
|
-
|
|
90
|
+
let report = calculcate_coverage(coverage, parse_html)
|
|
91
|
+
```
|
|
59
92
|
|
|
60
93
|
### Optional: coverage from `<style>` blocks
|
|
61
94
|
|
|
62
|
-
|
|
95
|
+
Coverage generators also create coverage ranges for `<style>` blocks in HTML. If this applies to your code you should provide a HTML parser that we use to 'scrape' the HTML in case the browser gives us not just plain CSS contents. Depending on where you run this analysis you can use:
|
|
63
96
|
|
|
64
97
|
1. Browser:
|
|
65
98
|
```ts
|
|
@@ -67,7 +100,7 @@ Covergae generators also create coverage ranges for `<style>` blocks in HTML. If
|
|
|
67
100
|
return new DOMParser().parseFromString(html, 'text/html')
|
|
68
101
|
}
|
|
69
102
|
```
|
|
70
|
-
1. Node (using [linkedom](https://github.com/WebReflection/linkedom) in this example):
|
|
103
|
+
1. Node (using [linkedom](https://github.com/WebReflection/linkedom) in this example, but other parsers could work, too):
|
|
71
104
|
|
|
72
105
|
```ts
|
|
73
106
|
// $ npm install linkedom
|
|
@@ -1,98 +1,98 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import { format as
|
|
3
|
-
import { tokenTypes as
|
|
4
|
-
let
|
|
5
|
-
|
|
6
|
-
text:
|
|
7
|
-
url:
|
|
8
|
-
ranges:
|
|
9
|
-
|
|
10
|
-
start:
|
|
11
|
-
end:
|
|
1
|
+
import * as h from "valibot";
|
|
2
|
+
import { format as F } from "@projectwallace/format-css";
|
|
3
|
+
import { tokenTypes as x, tokenize as $ } from "css-tree/tokenizer";
|
|
4
|
+
let J = h.array(
|
|
5
|
+
h.object({
|
|
6
|
+
text: h.optional(h.string()),
|
|
7
|
+
url: h.string(),
|
|
8
|
+
ranges: h.array(
|
|
9
|
+
h.object({
|
|
10
|
+
start: h.number(),
|
|
11
|
+
end: h.number()
|
|
12
12
|
})
|
|
13
13
|
)
|
|
14
14
|
})
|
|
15
15
|
);
|
|
16
|
-
function
|
|
17
|
-
return
|
|
16
|
+
function q(t) {
|
|
17
|
+
return h.safeParse(J, t).success;
|
|
18
18
|
}
|
|
19
|
-
function
|
|
19
|
+
function X(t) {
|
|
20
20
|
try {
|
|
21
21
|
let e = JSON.parse(t);
|
|
22
|
-
return
|
|
22
|
+
return q(e) ? e : [];
|
|
23
23
|
} catch {
|
|
24
24
|
return [];
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
function
|
|
28
|
-
return t.map(({ url: e, text:
|
|
29
|
-
if (!
|
|
30
|
-
return { url: e, text:
|
|
31
|
-
let
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
]), o =
|
|
40
|
-
function
|
|
41
|
-
let
|
|
42
|
-
for (let
|
|
43
|
-
if (
|
|
44
|
-
if (
|
|
45
|
-
return
|
|
46
|
-
|
|
27
|
+
function P(t) {
|
|
28
|
+
return t.map(({ url: e, text: s, ranges: r }) => {
|
|
29
|
+
if (!s)
|
|
30
|
+
return { url: e, text: s, ranges: r };
|
|
31
|
+
let c = F(s), f = /* @__PURE__ */ new Set([
|
|
32
|
+
x.EOF,
|
|
33
|
+
x.BadString,
|
|
34
|
+
x.BadUrl,
|
|
35
|
+
x.WhiteSpace,
|
|
36
|
+
x.Semicolon,
|
|
37
|
+
x.Comment,
|
|
38
|
+
x.Colon
|
|
39
|
+
]), o = r.map(({ start: a, end: i }) => ({ start: a, end: i, tokens: [] }));
|
|
40
|
+
function v(a, i) {
|
|
41
|
+
let l = 0;
|
|
42
|
+
for (let u of o) {
|
|
43
|
+
if (u.start > i) return -1;
|
|
44
|
+
if (u.start <= a && u.end >= i)
|
|
45
|
+
return l;
|
|
46
|
+
l++;
|
|
47
47
|
}
|
|
48
48
|
return -1;
|
|
49
49
|
}
|
|
50
|
-
let
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
let
|
|
55
|
-
|
|
50
|
+
let _ = 0;
|
|
51
|
+
$(s, (a, i, l) => {
|
|
52
|
+
if (f.has(a)) return;
|
|
53
|
+
_++, a === x.Url && (_ += 2);
|
|
54
|
+
let u = v(i, l);
|
|
55
|
+
u !== -1 && o[u].tokens.push(_);
|
|
56
56
|
});
|
|
57
|
-
let
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
let g = /* @__PURE__ */ new Map();
|
|
58
|
+
_ = 0, $(c, (a, i, l) => {
|
|
59
|
+
f.has(a) || (_++, a === x.Url && (_ += 2), g.set(_, { start: i, end: l }));
|
|
60
60
|
});
|
|
61
|
-
let
|
|
62
|
-
for (let
|
|
63
|
-
let
|
|
64
|
-
|
|
65
|
-
start:
|
|
66
|
-
end:
|
|
61
|
+
let y = [];
|
|
62
|
+
for (let a of o) {
|
|
63
|
+
let i = g.get(a.tokens.at(0)), l = g.get(a.tokens.at(-1));
|
|
64
|
+
i !== void 0 && l !== void 0 && y.push({
|
|
65
|
+
start: i.start,
|
|
66
|
+
end: l.end
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
|
-
return { url: e, text:
|
|
69
|
+
return { url: e, text: c, ranges: y };
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
|
-
function
|
|
72
|
+
function R(t) {
|
|
73
73
|
let e = /* @__PURE__ */ new Map();
|
|
74
|
-
for (let
|
|
75
|
-
let
|
|
76
|
-
if (e.has(
|
|
77
|
-
let
|
|
78
|
-
for (let o of
|
|
79
|
-
let
|
|
80
|
-
for (let
|
|
81
|
-
if (
|
|
82
|
-
|
|
74
|
+
for (let s of t) {
|
|
75
|
+
let r = s.text || "";
|
|
76
|
+
if (e.has(r)) {
|
|
77
|
+
let f = e.get(r).ranges;
|
|
78
|
+
for (let o of s.ranges) {
|
|
79
|
+
let v = !1;
|
|
80
|
+
for (let _ of f)
|
|
81
|
+
if (_.start === o.start && _.end === o.end) {
|
|
82
|
+
v = !0;
|
|
83
83
|
break;
|
|
84
84
|
}
|
|
85
|
-
|
|
85
|
+
v || f.push(o);
|
|
86
86
|
}
|
|
87
87
|
} else
|
|
88
|
-
e.set(
|
|
89
|
-
url:
|
|
90
|
-
ranges:
|
|
88
|
+
e.set(r, {
|
|
89
|
+
url: s.url,
|
|
90
|
+
ranges: s.ranges
|
|
91
91
|
});
|
|
92
92
|
}
|
|
93
93
|
return e;
|
|
94
94
|
}
|
|
95
|
-
function
|
|
95
|
+
function D(t) {
|
|
96
96
|
try {
|
|
97
97
|
let e = new URL(t);
|
|
98
98
|
return e.pathname.slice(e.pathname.lastIndexOf(".") + 1);
|
|
@@ -101,102 +101,123 @@ function P(t) {
|
|
|
101
101
|
return t.slice(e, t.indexOf("/", e) + 1);
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
-
function
|
|
105
|
-
let
|
|
106
|
-
for (let
|
|
107
|
-
let
|
|
108
|
-
if (!
|
|
109
|
-
|
|
110
|
-
let
|
|
111
|
-
for (let
|
|
112
|
-
|
|
113
|
-
start: o + (
|
|
114
|
-
end: o + (
|
|
104
|
+
function G(t, e, s) {
|
|
105
|
+
let r = t(e), c = "", f = [], o = 0, v = r.querySelectorAll("style");
|
|
106
|
+
for (let _ of Array.from(v)) {
|
|
107
|
+
let g = _.textContent;
|
|
108
|
+
if (!g.trim()) continue;
|
|
109
|
+
c += g;
|
|
110
|
+
let y = e.indexOf(g), a = y + g.length;
|
|
111
|
+
for (let i of s)
|
|
112
|
+
i.start >= y && i.end <= a && f.push({
|
|
113
|
+
start: o + (i.start - y),
|
|
114
|
+
end: o + (i.end - y)
|
|
115
115
|
});
|
|
116
|
-
o +=
|
|
116
|
+
o += g.length;
|
|
117
117
|
}
|
|
118
118
|
return {
|
|
119
|
-
css:
|
|
120
|
-
ranges:
|
|
119
|
+
css: c,
|
|
120
|
+
ranges: f
|
|
121
121
|
};
|
|
122
122
|
}
|
|
123
|
-
function
|
|
123
|
+
function H(t) {
|
|
124
124
|
return /<\/?(html|body|head|div|span|script|style)/i.test(t);
|
|
125
125
|
}
|
|
126
|
-
function
|
|
127
|
-
let
|
|
128
|
-
for (let
|
|
129
|
-
if (!
|
|
130
|
-
let
|
|
131
|
-
if (
|
|
132
|
-
if (
|
|
133
|
-
|
|
126
|
+
function K(t, e) {
|
|
127
|
+
let s = [];
|
|
128
|
+
for (let r of t) {
|
|
129
|
+
if (!r.text) continue;
|
|
130
|
+
let c = D(r.url).toLowerCase();
|
|
131
|
+
if (c !== "js") {
|
|
132
|
+
if (c === "css") {
|
|
133
|
+
s.push(r);
|
|
134
134
|
continue;
|
|
135
135
|
}
|
|
136
|
-
if (
|
|
136
|
+
if (H(r.text)) {
|
|
137
137
|
if (!e)
|
|
138
138
|
continue;
|
|
139
|
-
let { css:
|
|
140
|
-
|
|
141
|
-
url:
|
|
142
|
-
text:
|
|
139
|
+
let { css: f, ranges: o } = G(e, r.text, r.ranges);
|
|
140
|
+
s.push({
|
|
141
|
+
url: r.url,
|
|
142
|
+
text: f,
|
|
143
143
|
ranges: o
|
|
144
144
|
});
|
|
145
145
|
continue;
|
|
146
146
|
}
|
|
147
|
-
|
|
148
|
-
url:
|
|
149
|
-
text:
|
|
150
|
-
ranges:
|
|
147
|
+
s.push({
|
|
148
|
+
url: r.url,
|
|
149
|
+
text: r.text,
|
|
150
|
+
ranges: r.ranges
|
|
151
151
|
});
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
|
-
return
|
|
154
|
+
return s;
|
|
155
155
|
}
|
|
156
|
-
function
|
|
156
|
+
function j(t, e) {
|
|
157
157
|
return e === 0 ? 0 : t / e;
|
|
158
158
|
}
|
|
159
|
-
function
|
|
160
|
-
let
|
|
161
|
-
if (!
|
|
159
|
+
function Y(t, e) {
|
|
160
|
+
let s = t.length;
|
|
161
|
+
if (!q(t))
|
|
162
162
|
throw new TypeError("No valid coverage data found");
|
|
163
|
-
let
|
|
164
|
-
function
|
|
165
|
-
let
|
|
166
|
-
if (!
|
|
167
|
-
for (let
|
|
168
|
-
if (!(
|
|
169
|
-
if (
|
|
163
|
+
let r = K(t, e), c = P(r), f = R(c), o = Array.from(f).map(([l, { url: u, ranges: L }]) => {
|
|
164
|
+
function M(n, d) {
|
|
165
|
+
let b = d + n.length, N = b + 1, W = /^\s*$/.test(n), k = n.endsWith("}");
|
|
166
|
+
if (!W && !k) {
|
|
167
|
+
for (let m of L)
|
|
168
|
+
if (!(m.start > b || m.end < d)) {
|
|
169
|
+
if (m.start <= d && m.end >= b)
|
|
170
170
|
return !0;
|
|
171
|
-
if (
|
|
171
|
+
if (n.startsWith("@") && m.start > d && m.start < N)
|
|
172
172
|
return !0;
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
return !1;
|
|
176
176
|
}
|
|
177
|
-
let
|
|
178
|
-
`),
|
|
179
|
-
for (let
|
|
180
|
-
let
|
|
181
|
-
(
|
|
177
|
+
let S = l.split(`
|
|
178
|
+
`), O = S.length, p = new Uint8Array(O), C = 0, B = l.length, U = 0, E = 0;
|
|
179
|
+
for (let n = 0; n < S.length; n++) {
|
|
180
|
+
let d = S[n], b = E, W = E + d.length + 1, k = /^\s*$/.test(d), m = d.endsWith("}"), z = M(d, b), w = !1, T = n > 0 && p[n - 1] === 1;
|
|
181
|
+
(z && !m && !k || (k || m) && T || k && !T && M(S[n + 1], W)) && (w = !0), p[n] = w ? 1 : 0, w && (C++, U += d.length + 1), E = W;
|
|
182
|
+
}
|
|
183
|
+
let A = [
|
|
184
|
+
{
|
|
185
|
+
start_line: 1,
|
|
186
|
+
is_covered: p[0] === 1,
|
|
187
|
+
end_line: 0,
|
|
188
|
+
total_lines: 0
|
|
189
|
+
}
|
|
190
|
+
];
|
|
191
|
+
for (let n = 0; n < p.length; n++) {
|
|
192
|
+
let d = p[n];
|
|
193
|
+
if (n > 0 && d !== p[n - 1]) {
|
|
194
|
+
let b = A.at(-1);
|
|
195
|
+
b.end_line = n, b.total_lines = n - b.start_line + 1, A.push({
|
|
196
|
+
start_line: n,
|
|
197
|
+
is_covered: d === 1,
|
|
198
|
+
end_line: n,
|
|
199
|
+
total_lines: 0
|
|
200
|
+
});
|
|
201
|
+
}
|
|
182
202
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
203
|
+
let I = A.at(-1);
|
|
204
|
+
return I.total_lines = p.length - I.start_line + 1, I.end_line = p.length, {
|
|
205
|
+
url: u,
|
|
206
|
+
text: l,
|
|
207
|
+
ranges: L,
|
|
208
|
+
unused_bytes: B - U,
|
|
209
|
+
used_bytes: U,
|
|
210
|
+
total_bytes: B,
|
|
211
|
+
line_coverage_ratio: j(C, O),
|
|
212
|
+
byte_coverage_ratio: j(U, B),
|
|
213
|
+
line_coverage: p,
|
|
214
|
+
total_lines: O,
|
|
215
|
+
covered_lines: C,
|
|
216
|
+
uncovered_lines: O - C,
|
|
217
|
+
chunks: A
|
|
197
218
|
};
|
|
198
|
-
}), { total_lines:
|
|
199
|
-
(
|
|
219
|
+
}), { total_lines: v, total_covered_lines: _, total_uncovered_lines: g, total_bytes: y, total_used_bytes: a, total_unused_bytes: i } = o.reduce(
|
|
220
|
+
(l, u) => (l.total_lines += u.total_lines, l.total_covered_lines += u.covered_lines, l.total_uncovered_lines += u.uncovered_lines, l.total_bytes += u.total_bytes, l.total_used_bytes += u.used_bytes, l.total_unused_bytes += u.unused_bytes, l),
|
|
200
221
|
{
|
|
201
222
|
total_lines: 0,
|
|
202
223
|
total_covered_lines: 0,
|
|
@@ -207,20 +228,20 @@ function V(t, e) {
|
|
|
207
228
|
}
|
|
208
229
|
);
|
|
209
230
|
return {
|
|
210
|
-
total_files_found:
|
|
211
|
-
total_bytes:
|
|
212
|
-
total_lines:
|
|
213
|
-
used_bytes:
|
|
214
|
-
covered_lines:
|
|
215
|
-
unused_bytes:
|
|
216
|
-
uncovered_lines:
|
|
217
|
-
byte_coverage_ratio:
|
|
218
|
-
line_coverage_ratio:
|
|
231
|
+
total_files_found: s,
|
|
232
|
+
total_bytes: y,
|
|
233
|
+
total_lines: v,
|
|
234
|
+
used_bytes: a,
|
|
235
|
+
covered_lines: _,
|
|
236
|
+
unused_bytes: i,
|
|
237
|
+
uncovered_lines: g,
|
|
238
|
+
byte_coverage_ratio: j(a, y),
|
|
239
|
+
line_coverage_ratio: j(_, v),
|
|
219
240
|
coverage_per_stylesheet: o,
|
|
220
241
|
total_stylesheets: o.length
|
|
221
242
|
};
|
|
222
243
|
}
|
|
223
244
|
export {
|
|
224
|
-
|
|
225
|
-
|
|
245
|
+
Y as calculate_coverage,
|
|
246
|
+
X as parse_coverage
|
|
226
247
|
};
|
package/dist/src/index.d.ts
CHANGED
|
@@ -15,6 +15,12 @@ export type StylesheetCoverage = CoverageData & {
|
|
|
15
15
|
text: string;
|
|
16
16
|
ranges: Range[];
|
|
17
17
|
line_coverage: Uint8Array;
|
|
18
|
+
chunks: {
|
|
19
|
+
is_covered: boolean;
|
|
20
|
+
start_line: number;
|
|
21
|
+
end_line: number;
|
|
22
|
+
total_lines: number;
|
|
23
|
+
}[];
|
|
18
24
|
};
|
|
19
25
|
export type CoverageResult = CoverageData & {
|
|
20
26
|
total_files_found: number;
|