@projectwallace/css-code-coverage 0.9.0 → 0.10.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/dist/{cli.js → cli.mjs} +76 -97
- package/dist/index.d.ts +8 -22
- package/dist/index.js +59 -42
- package/package.json +30 -26
package/dist/{cli.js → cli.mjs}
RENAMED
|
@@ -1,52 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs, styleText } from "node:util";
|
|
3
|
-
import
|
|
3
|
+
import { join, resolve, sep } from "node:path";
|
|
4
4
|
import { calculate_coverage } from "@projectwallace/css-code-coverage";
|
|
5
5
|
import { readFile, readdir, stat } from "node:fs/promises";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
|
|
8
6
|
//#region src/cli/arguments.ts
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
let CoverageDirSchema = v.pipe(v.string(), v.nonEmpty());
|
|
20
|
-
let RatioPercentageSchema = v.pipe(v.string(), v.transform(Number), v.number(), v.minValue(0), v.maxValue(1));
|
|
21
|
-
let ShowUncoveredSchema = v.pipe(v.string(), v.enum(show_uncovered_options));
|
|
22
|
-
let ReporterSchema = v.pipe(v.string(), v.enum(reporters));
|
|
23
|
-
let CliArgumentsSchema = v.object({
|
|
24
|
-
"coverage-dir": CoverageDirSchema,
|
|
25
|
-
"min-coverage": RatioPercentageSchema,
|
|
26
|
-
"min-file-coverage": v.optional(RatioPercentageSchema),
|
|
27
|
-
"show-uncovered": v.optional(ShowUncoveredSchema, show_uncovered_options.violations),
|
|
28
|
-
reporter: v.optional(ReporterSchema, reporters.pretty)
|
|
29
|
-
});
|
|
30
|
-
var InvalidArgumentsError = class extends Error {
|
|
31
|
-
issues;
|
|
32
|
-
constructor(issues) {
|
|
33
|
-
super();
|
|
34
|
-
this.issues = issues;
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
function validate_arguments(args) {
|
|
38
|
-
let parse_result = v.safeParse(CliArgumentsSchema, args);
|
|
39
|
-
if (!parse_result.success) throw new InvalidArgumentsError(parse_result.issues.map((issue) => ({
|
|
40
|
-
path: issue.path?.map((path) => path.key).join("."),
|
|
41
|
-
message: issue.message
|
|
42
|
-
})));
|
|
43
|
-
return parse_result.output;
|
|
44
|
-
}
|
|
7
|
+
const SHOW_UNCOVERED = [
|
|
8
|
+
"none",
|
|
9
|
+
"all",
|
|
10
|
+
"violations"
|
|
11
|
+
];
|
|
12
|
+
const REPORTERS = [
|
|
13
|
+
"pretty",
|
|
14
|
+
"tap",
|
|
15
|
+
"json"
|
|
16
|
+
];
|
|
45
17
|
function parse_arguments(args) {
|
|
46
|
-
let { values } = parseArgs({
|
|
18
|
+
let { values, positionals } = parseArgs({
|
|
47
19
|
args,
|
|
20
|
+
allowPositionals: true,
|
|
48
21
|
options: {
|
|
49
|
-
"coverage-dir": { type: "string" },
|
|
50
22
|
"min-coverage": { type: "string" },
|
|
51
23
|
"min-file-coverage": {
|
|
52
24
|
type: "string",
|
|
@@ -62,9 +34,31 @@ function parse_arguments(args) {
|
|
|
62
34
|
}
|
|
63
35
|
}
|
|
64
36
|
});
|
|
65
|
-
|
|
37
|
+
let issues = [];
|
|
38
|
+
let coverage_dir = positionals[0];
|
|
39
|
+
if (!coverage_dir) issues.push("<coverage-dir> is required");
|
|
40
|
+
else {
|
|
41
|
+
let resolved = resolve(coverage_dir);
|
|
42
|
+
let cwd = process.cwd();
|
|
43
|
+
if (resolved !== cwd && !resolved.startsWith(cwd + sep)) issues.push("InvalidPath");
|
|
44
|
+
}
|
|
45
|
+
let min_coverage = Number(values["min-coverage"]);
|
|
46
|
+
if (values["min-coverage"] === void 0 || isNaN(min_coverage) || min_coverage < 0 || min_coverage > 1) issues.push("--min-coverage must be a number between 0 and 1");
|
|
47
|
+
let min_file_coverage = Number(values["min-file-coverage"]);
|
|
48
|
+
if (isNaN(min_file_coverage) || min_file_coverage < 0 || min_file_coverage > 1) issues.push("--min-file-coverage must be a number between 0 and 1");
|
|
49
|
+
let show_uncovered = values["show-uncovered"];
|
|
50
|
+
if (!SHOW_UNCOVERED.includes(show_uncovered)) issues.push(`--show-uncovered must be one of: ${SHOW_UNCOVERED.join(", ")}`);
|
|
51
|
+
let reporter = values["reporter"];
|
|
52
|
+
if (!REPORTERS.includes(reporter)) issues.push(`--reporter must be one of: ${REPORTERS.join(", ")}`);
|
|
53
|
+
if (issues.length > 0) throw new Error(issues.join("\n"));
|
|
54
|
+
return {
|
|
55
|
+
"coverage-dir": resolve(coverage_dir),
|
|
56
|
+
"min-coverage": min_coverage,
|
|
57
|
+
"min-file-coverage": min_file_coverage,
|
|
58
|
+
"show-uncovered": show_uncovered,
|
|
59
|
+
reporter
|
|
60
|
+
};
|
|
66
61
|
}
|
|
67
|
-
|
|
68
62
|
//#endregion
|
|
69
63
|
//#region src/cli/program.ts
|
|
70
64
|
function validate_min_line_coverage(actual, expected) {
|
|
@@ -99,30 +93,19 @@ function program({ min_coverage, min_file_coverage }, coverage_data) {
|
|
|
99
93
|
}
|
|
100
94
|
};
|
|
101
95
|
}
|
|
102
|
-
|
|
103
96
|
//#endregion
|
|
104
97
|
//#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
98
|
function is_valid_coverage(input) {
|
|
115
|
-
return
|
|
99
|
+
return Array.isArray(input) && input.every((item) => typeof item === "object" && item !== null && typeof item.text === "string" && typeof item.url === "string" && Array.isArray(item.ranges) && item.ranges.every((r) => typeof r === "object" && r !== null && typeof r.start === "number" && typeof r.end === "number"));
|
|
116
100
|
}
|
|
117
101
|
function parse_coverage(input) {
|
|
118
102
|
try {
|
|
119
|
-
let
|
|
120
|
-
return is_valid_coverage(
|
|
103
|
+
let parsed = JSON.parse(input);
|
|
104
|
+
return is_valid_coverage(parsed) ? parsed : [];
|
|
121
105
|
} catch {
|
|
122
106
|
return [];
|
|
123
107
|
}
|
|
124
108
|
}
|
|
125
|
-
|
|
126
109
|
//#endregion
|
|
127
110
|
//#region src/cli/file-reader.ts
|
|
128
111
|
async function read(coverage_dir) {
|
|
@@ -136,7 +119,6 @@ async function read(coverage_dir) {
|
|
|
136
119
|
}
|
|
137
120
|
return parsed_files;
|
|
138
121
|
}
|
|
139
|
-
|
|
140
122
|
//#endregion
|
|
141
123
|
//#region src/cli/reporters/pretty.ts
|
|
142
124
|
function indent(line) {
|
|
@@ -146,7 +128,7 @@ let line_number = (num) => `${num.toString().padStart(5, " ")} │ `;
|
|
|
146
128
|
function percentage(ratio, decimals = 2) {
|
|
147
129
|
return `${(ratio * 100).toFixed(ratio === 1 ? 0 : decimals)}%`;
|
|
148
130
|
}
|
|
149
|
-
function highlight(css, styleText
|
|
131
|
+
function highlight(css, styleText) {
|
|
150
132
|
if (css.trim().startsWith("@")) {
|
|
151
133
|
let at_pos = css.indexOf("@");
|
|
152
134
|
let space_pos = css.indexOf(" ", at_pos);
|
|
@@ -154,38 +136,38 @@ function highlight(css, styleText$1) {
|
|
|
154
136
|
let is_empty = css.endsWith("{}");
|
|
155
137
|
let prelude = css.slice(space_pos, is_empty ? -2 : -1);
|
|
156
138
|
return [
|
|
157
|
-
styleText
|
|
158
|
-
styleText
|
|
139
|
+
styleText("blueBright", name),
|
|
140
|
+
styleText("magentaBright", prelude),
|
|
159
141
|
is_empty ? "{}" : "{"
|
|
160
142
|
].join("");
|
|
161
143
|
}
|
|
162
144
|
if (css.includes(":") && css.endsWith(";")) return [
|
|
163
|
-
styleText
|
|
145
|
+
styleText("cyanBright", css.slice(0, css.indexOf(":"))),
|
|
164
146
|
":",
|
|
165
147
|
css.slice(css.indexOf(":") + 1, css.length - 1),
|
|
166
148
|
";"
|
|
167
149
|
].join("");
|
|
168
|
-
if (css.endsWith("{}")) return [styleText
|
|
150
|
+
if (css.endsWith("{}")) return [styleText("greenBright", css.slice(0, -2)), "{}"].join("");
|
|
169
151
|
if (css.endsWith("}")) return css;
|
|
170
152
|
if (css.trim() === "") return css;
|
|
171
|
-
if (css.endsWith(",")) return [styleText
|
|
172
|
-
return [styleText
|
|
153
|
+
if (css.endsWith(",")) return [styleText("greenBright", css.slice(0, -1)), ","].join("");
|
|
154
|
+
return [styleText("greenBright", css.slice(0, -1)), "{"].join("");
|
|
173
155
|
}
|
|
174
|
-
function print_lines({ report, context }, params, { styleText
|
|
156
|
+
function print_lines({ report, context }, params, { styleText, print_width }) {
|
|
175
157
|
let output = [];
|
|
176
|
-
if (report.min_line_coverage.ok) output.push(`${styleText
|
|
158
|
+
if (report.min_line_coverage.ok) output.push(`${styleText(["bold", "green"], "Success")}: total line coverage is ${percentage(report.min_line_coverage.actual)}`);
|
|
177
159
|
else {
|
|
178
160
|
let { actual, expected } = report.min_line_coverage;
|
|
179
|
-
output.push(`${styleText
|
|
161
|
+
output.push(`${styleText(["bold", "red"], "Failed")}: line coverage is ${percentage(actual)}% which is lower than the threshold of ${expected}`);
|
|
180
162
|
let lines_to_cover = expected * context.coverage.total_lines - context.coverage.covered_lines;
|
|
181
163
|
output.push(`Tip: cover ${Math.ceil(lines_to_cover)} more ${lines_to_cover === 1 ? "line" : "lines"} to meet the threshold of ${percentage(expected)}`);
|
|
182
164
|
}
|
|
183
165
|
if (report.min_file_line_coverage.expected !== void 0) {
|
|
184
|
-
let { expected, actual, ok
|
|
185
|
-
if (ok
|
|
166
|
+
let { expected, actual, ok } = report.min_file_line_coverage;
|
|
167
|
+
if (ok) output.push(`${styleText(["bold", "green"], "Success")}: all files pass minimum line coverage of ${percentage(expected)}`);
|
|
186
168
|
else {
|
|
187
169
|
let num_files_failed = context.coverage.coverage_per_stylesheet.filter((sheet) => sheet.line_coverage_ratio < expected).length;
|
|
188
|
-
output.push(`${styleText
|
|
170
|
+
output.push(`${styleText(["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)})`);
|
|
189
171
|
if (params["show-uncovered"] === "none") output.push(` Hint: set --show-uncovered=violations to see which files didn't pass`);
|
|
190
172
|
}
|
|
191
173
|
}
|
|
@@ -197,30 +179,30 @@ function print_lines({ report, context }, params, { styleText: styleText$1, prin
|
|
|
197
179
|
output.push();
|
|
198
180
|
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") {
|
|
199
181
|
output.push();
|
|
200
|
-
output.push(styleText
|
|
182
|
+
output.push(styleText("dim", "─".repeat(print_width)));
|
|
201
183
|
output.push(`File: ${sheet.url}`);
|
|
202
184
|
output.push(`Coverage: ${percentage(sheet.line_coverage_ratio)}, ${sheet.covered_lines}/${sheet.total_lines} lines covered`);
|
|
203
185
|
if (min_file_line_coverage && min_file_line_coverage !== 0 && sheet.line_coverage_ratio < min_file_line_coverage) {
|
|
204
186
|
let lines_to_cover = min_file_line_coverage * sheet.total_lines - sheet.covered_lines;
|
|
205
187
|
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)}`);
|
|
206
188
|
}
|
|
207
|
-
output.push(styleText
|
|
189
|
+
output.push(styleText("dim", "─".repeat(print_width)));
|
|
208
190
|
let lines = sheet.text.split("\n");
|
|
209
|
-
for (let chunk of sheet.chunks.filter((chunk
|
|
191
|
+
for (let chunk of sheet.chunks.filter((chunk) => !chunk.is_covered)) {
|
|
210
192
|
for (let x = Math.max(chunk.start_line - NUM_LEADING_LINES, 1); x < chunk.start_line; x++) output.push([
|
|
211
193
|
" ",
|
|
212
|
-
styleText
|
|
213
|
-
styleText
|
|
194
|
+
styleText("dim", line_number(x)),
|
|
195
|
+
styleText("dim", indent(lines[x - 1]))
|
|
214
196
|
].join(""));
|
|
215
197
|
for (let i = chunk.start_line; i <= chunk.end_line; i++) output.push([
|
|
216
|
-
styleText
|
|
217
|
-
styleText
|
|
218
|
-
highlight(indent(lines[i - 1]), styleText
|
|
198
|
+
styleText("red", "▌"),
|
|
199
|
+
styleText("dim", line_number(i)),
|
|
200
|
+
highlight(indent(lines[i - 1]), styleText)
|
|
219
201
|
].join(""));
|
|
220
202
|
for (let y = chunk.end_line + 1; y < Math.min(chunk.end_line + NUM_TRAILING_LINES + 1, lines.length); y++) output.push([
|
|
221
203
|
" ",
|
|
222
|
-
styleText
|
|
223
|
-
styleText
|
|
204
|
+
styleText("dim", line_number(y)),
|
|
205
|
+
styleText("dim", indent(lines[y - 1]))
|
|
224
206
|
].join(""));
|
|
225
207
|
output.push("");
|
|
226
208
|
}
|
|
@@ -228,14 +210,13 @@ function print_lines({ report, context }, params, { styleText: styleText$1, prin
|
|
|
228
210
|
}
|
|
229
211
|
return output;
|
|
230
212
|
}
|
|
231
|
-
function print(report, params) {
|
|
213
|
+
function print$2(report, params) {
|
|
232
214
|
let logger = report.report.ok ? console.log : console.error;
|
|
233
215
|
for (let line of print_lines(report, params, {
|
|
234
216
|
styleText,
|
|
235
217
|
print_width: process.stdout.columns
|
|
236
218
|
})) logger(line);
|
|
237
219
|
}
|
|
238
|
-
|
|
239
220
|
//#endregion
|
|
240
221
|
//#region src/cli/reporters/tap.ts
|
|
241
222
|
function version() {
|
|
@@ -291,7 +272,6 @@ function print$1({ report, context }, _params) {
|
|
|
291
272
|
}
|
|
292
273
|
}
|
|
293
274
|
}
|
|
294
|
-
|
|
295
275
|
//#endregion
|
|
296
276
|
//#region src/cli/reporters/json.ts
|
|
297
277
|
function prepare({ report, context }, params) {
|
|
@@ -305,7 +285,7 @@ function prepare({ report, context }, params) {
|
|
|
305
285
|
context
|
|
306
286
|
};
|
|
307
287
|
}
|
|
308
|
-
function print
|
|
288
|
+
function print({ report, context }, params) {
|
|
309
289
|
let logger = report.ok ? console.log : console.error;
|
|
310
290
|
let data = prepare({
|
|
311
291
|
context,
|
|
@@ -313,17 +293,18 @@ function print$2({ report, context }, params) {
|
|
|
313
293
|
}, params);
|
|
314
294
|
logger(JSON.stringify(data));
|
|
315
295
|
}
|
|
316
|
-
|
|
317
296
|
//#endregion
|
|
318
297
|
//#region src/cli/help.ts
|
|
319
298
|
function help() {
|
|
320
299
|
return `
|
|
321
300
|
${styleText(["bold"], "USAGE")}
|
|
322
|
-
$ css-coverage
|
|
301
|
+
$ css-coverage <coverage-dir> --min-coverage=<number> [options]
|
|
302
|
+
|
|
303
|
+
${styleText("bold", "ARGUMENTS")}
|
|
304
|
+
<coverage-dir> Where your Coverage JSON files are
|
|
323
305
|
|
|
324
306
|
${styleText("bold", "OPTIONS")}
|
|
325
307
|
Required:
|
|
326
|
-
--coverage-dir Where your Coverage JSON files are
|
|
327
308
|
--min-coverage Minimum overall CSS coverage [0-1]
|
|
328
309
|
|
|
329
310
|
Optional:
|
|
@@ -342,21 +323,20 @@ Optional:
|
|
|
342
323
|
|
|
343
324
|
${styleText("bold", "EXAMPLES")}
|
|
344
325
|
${styleText("dim", "# analyze all .json files in ./coverage; require 80% overall coverage")}
|
|
345
|
-
$ css-coverage
|
|
326
|
+
$ css-coverage ./coverage --min-coverage=0.8
|
|
346
327
|
|
|
347
328
|
${styleText("dim", "# Require 50% coverage per file")}
|
|
348
|
-
$ css-coverage
|
|
329
|
+
$ css-coverage ./coverage --min-coverage=0.8 --min-file-coverage=0.5
|
|
349
330
|
|
|
350
331
|
${styleText("dim", "Report JSON")}
|
|
351
|
-
$ css-coverage
|
|
332
|
+
$ css-coverage ./coverage --min-coverage=0.8 --reporter=json
|
|
352
333
|
`.trim();
|
|
353
334
|
}
|
|
354
|
-
|
|
355
335
|
//#endregion
|
|
356
336
|
//#region src/cli/cli.ts
|
|
357
337
|
async function cli(cli_args) {
|
|
358
338
|
if (!cli_args || cli_args.length === 0 || cli_args.includes("--help") || cli_args.includes("-h")) return console.log(help());
|
|
359
|
-
let params =
|
|
339
|
+
let params = parse_arguments(cli_args);
|
|
360
340
|
let coverage_data = await read(params["coverage-dir"]);
|
|
361
341
|
let report = program({
|
|
362
342
|
min_coverage: params["min-coverage"],
|
|
@@ -364,8 +344,8 @@ async function cli(cli_args) {
|
|
|
364
344
|
}, coverage_data);
|
|
365
345
|
if (report.report.ok === false) process.exitCode = 1;
|
|
366
346
|
if (params.reporter === "tap") return print$1(report, params);
|
|
367
|
-
if (params.reporter === "json") return print
|
|
368
|
-
return print(report, params);
|
|
347
|
+
if (params.reporter === "json") return print(report, params);
|
|
348
|
+
return print$2(report, params);
|
|
369
349
|
}
|
|
370
350
|
try {
|
|
371
351
|
await cli(process.argv.slice(2));
|
|
@@ -373,6 +353,5 @@ try {
|
|
|
373
353
|
console.error(error);
|
|
374
354
|
process.exit(1);
|
|
375
355
|
}
|
|
376
|
-
|
|
377
356
|
//#endregion
|
|
378
|
-
export {
|
|
357
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,28 +1,14 @@
|
|
|
1
|
-
import * as v from "valibot";
|
|
2
|
-
|
|
3
1
|
//#region src/lib/parse-coverage.d.ts
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
type
|
|
9
|
-
declare let CoverageSchema: v.ObjectSchema<{
|
|
10
|
-
readonly text: v.StringSchema<undefined>;
|
|
11
|
-
readonly url: v.StringSchema<undefined>;
|
|
12
|
-
readonly ranges: v.ArraySchema<v.ObjectSchema<{
|
|
13
|
-
readonly start: v.NumberSchema<undefined>;
|
|
14
|
-
readonly end: v.NumberSchema<undefined>;
|
|
15
|
-
}, undefined>, undefined>;
|
|
16
|
-
}, undefined>;
|
|
17
|
-
type Coverage = v.InferInput<typeof CoverageSchema>;
|
|
18
|
-
declare function parse_coverage(input: string): {
|
|
2
|
+
type Range = {
|
|
3
|
+
start: number;
|
|
4
|
+
end: number;
|
|
5
|
+
};
|
|
6
|
+
type Coverage = {
|
|
19
7
|
text: string;
|
|
20
8
|
url: string;
|
|
21
|
-
ranges:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}[];
|
|
25
|
-
}[];
|
|
9
|
+
ranges: Range[];
|
|
10
|
+
};
|
|
11
|
+
declare function parse_coverage(input: string): Coverage[];
|
|
26
12
|
//#endregion
|
|
27
13
|
//#region src/lib/chunkify.d.ts
|
|
28
14
|
type Chunk = {
|
package/dist/index.js
CHANGED
|
@@ -1,28 +1,17 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { tokenize } from "@projectwallace/css-parser/tokenizer";
|
|
2
2
|
import { format } from "@projectwallace/format-css";
|
|
3
|
-
|
|
4
3
|
//#region src/lib/parse-coverage.ts
|
|
5
|
-
let RangeSchema = v.object({
|
|
6
|
-
start: v.number(),
|
|
7
|
-
end: v.number()
|
|
8
|
-
});
|
|
9
|
-
let CoverageSchema = v.object({
|
|
10
|
-
text: v.string(),
|
|
11
|
-
url: v.string(),
|
|
12
|
-
ranges: v.array(RangeSchema)
|
|
13
|
-
});
|
|
14
4
|
function is_valid_coverage(input) {
|
|
15
|
-
return
|
|
5
|
+
return Array.isArray(input) && input.every((item) => typeof item === "object" && item !== null && typeof item.text === "string" && typeof item.url === "string" && Array.isArray(item.ranges) && item.ranges.every((r) => typeof r === "object" && r !== null && typeof r.start === "number" && typeof r.end === "number"));
|
|
16
6
|
}
|
|
17
7
|
function parse_coverage(input) {
|
|
18
8
|
try {
|
|
19
|
-
let
|
|
20
|
-
return is_valid_coverage(
|
|
9
|
+
let parsed = JSON.parse(input);
|
|
10
|
+
return is_valid_coverage(parsed) ? parsed : [];
|
|
21
11
|
} catch {
|
|
22
12
|
return [];
|
|
23
13
|
}
|
|
24
14
|
}
|
|
25
|
-
|
|
26
15
|
//#endregion
|
|
27
16
|
//#region src/lib/chunkify.ts
|
|
28
17
|
const WHITESPACE_ONLY_REGEX = /^\s+$/;
|
|
@@ -38,7 +27,7 @@ function merge(stylesheet) {
|
|
|
38
27
|
latest_chunk.end_offset = chunk.end_offset;
|
|
39
28
|
previous_chunk = chunk;
|
|
40
29
|
continue;
|
|
41
|
-
} else if (
|
|
30
|
+
} else if (chunk.end_offset === chunk.start_offset) {
|
|
42
31
|
latest_chunk.end_offset = chunk.end_offset;
|
|
43
32
|
continue;
|
|
44
33
|
}
|
|
@@ -51,6 +40,48 @@ function merge(stylesheet) {
|
|
|
51
40
|
chunks: new_chunks
|
|
52
41
|
};
|
|
53
42
|
}
|
|
43
|
+
function mark_comments_as_covered(stylesheet) {
|
|
44
|
+
let new_chunks = [];
|
|
45
|
+
for (let chunk of stylesheet.chunks) {
|
|
46
|
+
if (chunk.is_covered) {
|
|
47
|
+
new_chunks.push(chunk);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
let text = stylesheet.text.slice(chunk.start_offset, chunk.end_offset);
|
|
51
|
+
let comments = [];
|
|
52
|
+
for (const _ of tokenize(text, ({ start, end }) => comments.push({
|
|
53
|
+
start,
|
|
54
|
+
end
|
|
55
|
+
})));
|
|
56
|
+
if (comments.length === 0) {
|
|
57
|
+
new_chunks.push(chunk);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
let last_end = 0;
|
|
61
|
+
for (let comment of comments) {
|
|
62
|
+
if (comment.start > last_end) new_chunks.push({
|
|
63
|
+
start_offset: chunk.start_offset + last_end,
|
|
64
|
+
end_offset: chunk.start_offset + comment.start,
|
|
65
|
+
is_covered: false
|
|
66
|
+
});
|
|
67
|
+
new_chunks.push({
|
|
68
|
+
start_offset: chunk.start_offset + comment.start,
|
|
69
|
+
end_offset: chunk.start_offset + comment.end,
|
|
70
|
+
is_covered: true
|
|
71
|
+
});
|
|
72
|
+
last_end = comment.end;
|
|
73
|
+
}
|
|
74
|
+
if (last_end < text.length) new_chunks.push({
|
|
75
|
+
start_offset: chunk.start_offset + last_end,
|
|
76
|
+
end_offset: chunk.end_offset,
|
|
77
|
+
is_covered: false
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return merge({
|
|
81
|
+
...stylesheet,
|
|
82
|
+
chunks: new_chunks
|
|
83
|
+
});
|
|
84
|
+
}
|
|
54
85
|
function chunkify(stylesheet) {
|
|
55
86
|
let chunks = [];
|
|
56
87
|
let offset = 0;
|
|
@@ -70,7 +101,7 @@ function chunkify(stylesheet) {
|
|
|
70
101
|
});
|
|
71
102
|
offset = range.end;
|
|
72
103
|
}
|
|
73
|
-
if (offset
|
|
104
|
+
if (offset < stylesheet.text.length) chunks.push({
|
|
74
105
|
start_offset: offset,
|
|
75
106
|
end_offset: stylesheet.text.length,
|
|
76
107
|
is_covered: false
|
|
@@ -81,14 +112,13 @@ function chunkify(stylesheet) {
|
|
|
81
112
|
chunks
|
|
82
113
|
});
|
|
83
114
|
}
|
|
84
|
-
|
|
85
115
|
//#endregion
|
|
86
116
|
//#region src/lib/prettify.ts
|
|
87
117
|
function prettify(stylesheet) {
|
|
88
118
|
let line = 1;
|
|
89
119
|
let offset = 0;
|
|
90
120
|
let pretty_chunks = stylesheet.chunks.map((chunk, index) => {
|
|
91
|
-
let css = format(stylesheet.text.substring(chunk.start_offset, chunk.end_offset
|
|
121
|
+
let css = format(stylesheet.text.substring(chunk.start_offset, chunk.end_offset)).trim();
|
|
92
122
|
if (chunk.is_covered) {
|
|
93
123
|
let is_last_chunk = index === stylesheet.chunks.length - 1;
|
|
94
124
|
if (index === 0) css = css + (is_last_chunk ? "" : "\n");
|
|
@@ -118,7 +148,6 @@ function prettify(stylesheet) {
|
|
|
118
148
|
text: pretty_chunks.map(({ css }) => css).join("\n")
|
|
119
149
|
};
|
|
120
150
|
}
|
|
121
|
-
|
|
122
151
|
//#endregion
|
|
123
152
|
//#region src/lib/decuplicate.ts
|
|
124
153
|
function merge_ranges(ranges) {
|
|
@@ -141,14 +170,7 @@ function merge_entry_ranges(sheet, entry) {
|
|
|
141
170
|
url: entry.url,
|
|
142
171
|
ranges: [...entry.ranges]
|
|
143
172
|
};
|
|
144
|
-
let
|
|
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 });
|
|
150
|
-
}
|
|
151
|
-
}
|
|
173
|
+
for (let range of entry.ranges) sheet.ranges.push({ ...range });
|
|
152
174
|
return sheet;
|
|
153
175
|
}
|
|
154
176
|
function deduplicate_entries(entries) {
|
|
@@ -163,7 +185,6 @@ function deduplicate_entries(entries) {
|
|
|
163
185
|
ranges: merge_ranges(ranges)
|
|
164
186
|
}));
|
|
165
187
|
}
|
|
166
|
-
|
|
167
188
|
//#endregion
|
|
168
189
|
//#region src/lib/ext.ts
|
|
169
190
|
function ext(url) {
|
|
@@ -175,7 +196,6 @@ function ext(url) {
|
|
|
175
196
|
return url.slice(ext_index, url.indexOf("/", ext_index) + 1);
|
|
176
197
|
}
|
|
177
198
|
}
|
|
178
|
-
|
|
179
199
|
//#endregion
|
|
180
200
|
//#region src/lib/html-parser.ts
|
|
181
201
|
/**
|
|
@@ -204,7 +224,6 @@ var DOMParser = class {
|
|
|
204
224
|
} };
|
|
205
225
|
}
|
|
206
226
|
};
|
|
207
|
-
|
|
208
227
|
//#endregion
|
|
209
228
|
//#region src/lib/remap-html.ts
|
|
210
229
|
function get_dom_parser() {
|
|
@@ -217,12 +236,14 @@ function remap_html(html, old_ranges) {
|
|
|
217
236
|
let new_ranges = [];
|
|
218
237
|
let current_offset = 0;
|
|
219
238
|
let style_elements = doc.querySelectorAll("style");
|
|
239
|
+
let search_from = 0;
|
|
220
240
|
for (let style_element of Array.from(style_elements)) {
|
|
221
241
|
let style_content = style_element.textContent;
|
|
222
242
|
if (!style_content.trim()) continue;
|
|
223
243
|
combined_css += style_content;
|
|
224
|
-
let start_index = html.indexOf(style_content);
|
|
244
|
+
let start_index = html.indexOf(style_content, search_from);
|
|
225
245
|
let end_index = start_index + style_content.length;
|
|
246
|
+
search_from = end_index;
|
|
226
247
|
for (let range of old_ranges) if (range.start >= start_index && range.end <= end_index) new_ranges.push({
|
|
227
248
|
start: current_offset + (range.start - start_index),
|
|
228
249
|
end: current_offset + (range.end - start_index)
|
|
@@ -234,7 +255,6 @@ function remap_html(html, old_ranges) {
|
|
|
234
255
|
ranges: new_ranges
|
|
235
256
|
};
|
|
236
257
|
}
|
|
237
|
-
|
|
238
258
|
//#endregion
|
|
239
259
|
//#region src/lib/filter-entries.ts
|
|
240
260
|
function is_html(text) {
|
|
@@ -279,7 +299,6 @@ function filter_coverage(acc, entry) {
|
|
|
279
299
|
}
|
|
280
300
|
return acc;
|
|
281
301
|
}
|
|
282
|
-
|
|
283
302
|
//#endregion
|
|
284
303
|
//#region src/lib/extend-ranges.ts
|
|
285
304
|
const AT_SIGN = 64;
|
|
@@ -296,12 +315,12 @@ function extend_ranges(coverage) {
|
|
|
296
315
|
if (text.charCodeAt(char_position) === AT_SIGN) {
|
|
297
316
|
range.start = char_position;
|
|
298
317
|
let next_offset = range.end;
|
|
299
|
-
let next_char
|
|
300
|
-
while (/\s/.test(next_char
|
|
318
|
+
let next_char = text.charAt(next_offset);
|
|
319
|
+
while (/\s/.test(next_char)) {
|
|
301
320
|
next_offset++;
|
|
302
|
-
next_char
|
|
321
|
+
next_char = text.charAt(next_offset);
|
|
303
322
|
}
|
|
304
|
-
if (next_char
|
|
323
|
+
if (next_char === "{") range.end = range.end + 1;
|
|
305
324
|
break;
|
|
306
325
|
}
|
|
307
326
|
}
|
|
@@ -317,7 +336,6 @@ function extend_ranges(coverage) {
|
|
|
317
336
|
url
|
|
318
337
|
};
|
|
319
338
|
}
|
|
320
|
-
|
|
321
339
|
//#endregion
|
|
322
340
|
//#region src/lib/index.ts
|
|
323
341
|
function ratio(fraction, total) {
|
|
@@ -360,7 +378,7 @@ function calculate_stylesheet_coverage({ text, url, chunks }) {
|
|
|
360
378
|
}
|
|
361
379
|
function calculate_coverage(coverage) {
|
|
362
380
|
let total_files_found = coverage.length;
|
|
363
|
-
let coverage_per_stylesheet = coverage.reduce((acc, entry) => filter_coverage(acc, entry), [])
|
|
381
|
+
let coverage_per_stylesheet = deduplicate_entries(coverage.reduce((acc, entry) => filter_coverage(acc, entry), [])).map((coverage) => extend_ranges(coverage)).map((sheet) => mark_comments_as_covered(chunkify(sheet))).map((sheet) => prettify(sheet)).map((stylesheet) => calculate_stylesheet_coverage(stylesheet));
|
|
364
382
|
let { total_lines, total_covered_lines, total_uncovered_lines, total_bytes, total_used_bytes, total_unused_bytes } = coverage_per_stylesheet.reduce((totals, sheet) => {
|
|
365
383
|
totals.total_lines += sheet.total_lines;
|
|
366
384
|
totals.total_covered_lines += sheet.covered_lines;
|
|
@@ -391,6 +409,5 @@ function calculate_coverage(coverage) {
|
|
|
391
409
|
total_stylesheets: coverage_per_stylesheet.length
|
|
392
410
|
};
|
|
393
411
|
}
|
|
394
|
-
|
|
395
412
|
//#endregion
|
|
396
|
-
export { calculate_coverage, parse_coverage };
|
|
413
|
+
export { calculate_coverage, parse_coverage };
|
package/package.json
CHANGED
|
@@ -1,63 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@projectwallace/css-code-coverage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Generate useful CSS Code Coverage report from browser-reported coverage",
|
|
5
|
-
"author": "Bart Veneman <bart@projectwallace.com>",
|
|
6
|
-
"repository": {
|
|
7
|
-
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/projectwallace/css-code-coverage.git"
|
|
9
|
-
},
|
|
10
|
-
"issues": "https://github.com/projectwallace/css-code-coverage/issues",
|
|
11
|
-
"homepage": "https://github.com/projectwallace/css-code-coverage",
|
|
12
5
|
"keywords": [
|
|
13
|
-
"css",
|
|
14
6
|
"code",
|
|
15
7
|
"coverage",
|
|
8
|
+
"css",
|
|
9
|
+
"dead",
|
|
10
|
+
"styles",
|
|
16
11
|
"testing",
|
|
17
|
-
"used",
|
|
18
12
|
"unused",
|
|
19
|
-
"
|
|
20
|
-
"styles"
|
|
13
|
+
"used"
|
|
21
14
|
],
|
|
15
|
+
"homepage": "https://github.com/projectwallace/css-code-coverage",
|
|
22
16
|
"license": "EUPL-1.2",
|
|
23
|
-
"
|
|
24
|
-
|
|
17
|
+
"author": "Bart Veneman <bart@projectwallace.com>",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/projectwallace/css-code-coverage.git"
|
|
21
|
+
},
|
|
22
|
+
"bin": {
|
|
23
|
+
"css-coverage": "dist/cli.mjs"
|
|
25
24
|
},
|
|
26
|
-
"type": "module",
|
|
27
25
|
"files": [
|
|
28
26
|
"dist"
|
|
29
27
|
],
|
|
30
|
-
"
|
|
31
|
-
"css-coverage": "dist/cli.js"
|
|
32
|
-
},
|
|
28
|
+
"type": "module",
|
|
33
29
|
"main": "dist/index.js",
|
|
30
|
+
"types": "dist/index.d.ts",
|
|
34
31
|
"exports": {
|
|
35
32
|
"types": "./dist/index.d.ts",
|
|
36
33
|
"default": "./dist/index.js"
|
|
37
34
|
},
|
|
38
|
-
"types": "dist/index.d.ts",
|
|
39
35
|
"scripts": {
|
|
40
36
|
"test": "c8 --reporter=text --reporter=lcov playwright test",
|
|
41
37
|
"build": "tsdown",
|
|
42
38
|
"check": "tsc --noEmit",
|
|
43
|
-
"lint": "oxlint --config .oxlintrc.json",
|
|
39
|
+
"lint": "oxlint --config .oxlintrc.json; oxfmt --check",
|
|
44
40
|
"lint-package": "publint",
|
|
45
41
|
"knip": "knip"
|
|
46
42
|
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@projectwallace/css-parser": "^0.13.8",
|
|
45
|
+
"@projectwallace/format-css": "^2.2.6"
|
|
46
|
+
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@codecov/vite-plugin": "^1.9.1",
|
|
49
49
|
"@playwright/test": "^1.57.0",
|
|
50
50
|
"@projectwallace/preset-oxlint": "^0.0.7",
|
|
51
51
|
"@types/node": "^25.0.3",
|
|
52
|
-
"c8": "^
|
|
52
|
+
"c8": "^11.0.0",
|
|
53
53
|
"knip": "^5.82.0",
|
|
54
|
+
"oxfmt": "^0.41.0",
|
|
54
55
|
"oxlint": "^1.39.0",
|
|
55
56
|
"publint": "^0.3.16",
|
|
56
|
-
"tsdown": "^0.
|
|
57
|
+
"tsdown": "^0.21.2",
|
|
57
58
|
"typescript": "^5.9.3"
|
|
58
59
|
},
|
|
59
|
-
"
|
|
60
|
-
"@projectwallace/
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
"overrides": {
|
|
61
|
+
"@projectwallace/css-parser": "$@projectwallace/css-parser"
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=20"
|
|
65
|
+
},
|
|
66
|
+
"issues": "https://github.com/projectwallace/css-code-coverage/issues"
|
|
63
67
|
}
|