@projectwallace/css-code-coverage 0.8.2 → 0.9.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/dist/{cli.js → cli.mjs} +97 -83
- package/dist/index.d.ts +8 -22
- package/dist/index.js +60 -43
- package/package.json +65 -60
package/dist/{cli.js → cli.mjs}
RENAMED
|
@@ -1,47 +1,19 @@
|
|
|
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
18
|
let { values } = parseArgs({
|
|
47
19
|
args,
|
|
@@ -62,9 +34,31 @@ function parse_arguments(args) {
|
|
|
62
34
|
}
|
|
63
35
|
}
|
|
64
36
|
});
|
|
65
|
-
|
|
37
|
+
let issues = [];
|
|
38
|
+
let coverage_dir = values["coverage-dir"];
|
|
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,31 +119,55 @@ 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) {
|
|
143
125
|
return (line || "").replace(/^\t+/, (tabs) => " ".repeat(tabs.length * 4));
|
|
144
126
|
}
|
|
145
|
-
let line_number = (num
|
|
127
|
+
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
|
|
131
|
+
function highlight(css, styleText) {
|
|
132
|
+
if (css.trim().startsWith("@")) {
|
|
133
|
+
let at_pos = css.indexOf("@");
|
|
134
|
+
let space_pos = css.indexOf(" ", at_pos);
|
|
135
|
+
let name = css.slice(0, space_pos);
|
|
136
|
+
let is_empty = css.endsWith("{}");
|
|
137
|
+
let prelude = css.slice(space_pos, is_empty ? -2 : -1);
|
|
138
|
+
return [
|
|
139
|
+
styleText("blueBright", name),
|
|
140
|
+
styleText("magentaBright", prelude),
|
|
141
|
+
is_empty ? "{}" : "{"
|
|
142
|
+
].join("");
|
|
143
|
+
}
|
|
144
|
+
if (css.includes(":") && css.endsWith(";")) return [
|
|
145
|
+
styleText("cyanBright", css.slice(0, css.indexOf(":"))),
|
|
146
|
+
":",
|
|
147
|
+
css.slice(css.indexOf(":") + 1, css.length - 1),
|
|
148
|
+
";"
|
|
149
|
+
].join("");
|
|
150
|
+
if (css.endsWith("{}")) return [styleText("greenBright", css.slice(0, -2)), "{}"].join("");
|
|
151
|
+
if (css.endsWith("}")) return css;
|
|
152
|
+
if (css.trim() === "") return css;
|
|
153
|
+
if (css.endsWith(",")) return [styleText("greenBright", css.slice(0, -1)), ","].join("");
|
|
154
|
+
return [styleText("greenBright", css.slice(0, -1)), "{"].join("");
|
|
155
|
+
}
|
|
156
|
+
function print_lines({ report, context }, params, { styleText, print_width }) {
|
|
150
157
|
let output = [];
|
|
151
|
-
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)}`);
|
|
152
159
|
else {
|
|
153
160
|
let { actual, expected } = report.min_line_coverage;
|
|
154
|
-
output.push(`${styleText
|
|
161
|
+
output.push(`${styleText(["bold", "red"], "Failed")}: line coverage is ${percentage(actual)}% which is lower than the threshold of ${expected}`);
|
|
155
162
|
let lines_to_cover = expected * context.coverage.total_lines - context.coverage.covered_lines;
|
|
156
163
|
output.push(`Tip: cover ${Math.ceil(lines_to_cover)} more ${lines_to_cover === 1 ? "line" : "lines"} to meet the threshold of ${percentage(expected)}`);
|
|
157
164
|
}
|
|
158
165
|
if (report.min_file_line_coverage.expected !== void 0) {
|
|
159
|
-
let { expected, actual, ok
|
|
160
|
-
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)}`);
|
|
161
168
|
else {
|
|
162
169
|
let num_files_failed = context.coverage.coverage_per_stylesheet.filter((sheet) => sheet.line_coverage_ratio < expected).length;
|
|
163
|
-
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)})`);
|
|
164
171
|
if (params["show-uncovered"] === "none") output.push(` Hint: set --show-uncovered=violations to see which files didn't pass`);
|
|
165
172
|
}
|
|
166
173
|
}
|
|
@@ -172,33 +179,44 @@ function print_lines({ report, context }, params, { styleText: styleText$1, prin
|
|
|
172
179
|
output.push();
|
|
173
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") {
|
|
174
181
|
output.push();
|
|
175
|
-
output.push(styleText
|
|
176
|
-
output.push(sheet.url);
|
|
182
|
+
output.push(styleText("dim", "─".repeat(print_width)));
|
|
183
|
+
output.push(`File: ${sheet.url}`);
|
|
177
184
|
output.push(`Coverage: ${percentage(sheet.line_coverage_ratio)}, ${sheet.covered_lines}/${sheet.total_lines} lines covered`);
|
|
178
185
|
if (min_file_line_coverage && min_file_line_coverage !== 0 && sheet.line_coverage_ratio < min_file_line_coverage) {
|
|
179
186
|
let lines_to_cover = min_file_line_coverage * sheet.total_lines - sheet.covered_lines;
|
|
180
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)}`);
|
|
181
188
|
}
|
|
182
|
-
output.push(styleText
|
|
189
|
+
output.push(styleText("dim", "─".repeat(print_width)));
|
|
183
190
|
let lines = sheet.text.split("\n");
|
|
184
|
-
for (let chunk of sheet.chunks.filter((chunk
|
|
185
|
-
for (let x = Math.max(chunk.start_line - NUM_LEADING_LINES, 1); x < chunk.start_line; x++) output.push([
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
191
|
+
for (let chunk of sheet.chunks.filter((chunk) => !chunk.is_covered)) {
|
|
192
|
+
for (let x = Math.max(chunk.start_line - NUM_LEADING_LINES, 1); x < chunk.start_line; x++) output.push([
|
|
193
|
+
" ",
|
|
194
|
+
styleText("dim", line_number(x)),
|
|
195
|
+
styleText("dim", indent(lines[x - 1]))
|
|
196
|
+
].join(""));
|
|
197
|
+
for (let i = chunk.start_line; i <= chunk.end_line; i++) output.push([
|
|
198
|
+
styleText("red", "▌"),
|
|
199
|
+
styleText("dim", line_number(i)),
|
|
200
|
+
highlight(indent(lines[i - 1]), styleText)
|
|
201
|
+
].join(""));
|
|
202
|
+
for (let y = chunk.end_line + 1; y < Math.min(chunk.end_line + NUM_TRAILING_LINES + 1, lines.length); y++) output.push([
|
|
203
|
+
" ",
|
|
204
|
+
styleText("dim", line_number(y)),
|
|
205
|
+
styleText("dim", indent(lines[y - 1]))
|
|
206
|
+
].join(""));
|
|
207
|
+
output.push("");
|
|
189
208
|
}
|
|
190
209
|
}
|
|
191
210
|
}
|
|
192
211
|
return output;
|
|
193
212
|
}
|
|
194
|
-
function print(report, params) {
|
|
213
|
+
function print$2(report, params) {
|
|
195
214
|
let logger = report.report.ok ? console.log : console.error;
|
|
196
215
|
for (let line of print_lines(report, params, {
|
|
197
216
|
styleText,
|
|
198
217
|
print_width: process.stdout.columns
|
|
199
218
|
})) logger(line);
|
|
200
219
|
}
|
|
201
|
-
|
|
202
220
|
//#endregion
|
|
203
221
|
//#region src/cli/reporters/tap.ts
|
|
204
222
|
function version() {
|
|
@@ -218,7 +236,7 @@ function meta(data) {
|
|
|
218
236
|
for (let key in data) console.log(` ${key}: ${data[key]}`);
|
|
219
237
|
console.log(" ...");
|
|
220
238
|
}
|
|
221
|
-
function print$1({ report, context },
|
|
239
|
+
function print$1({ report, context }, _params) {
|
|
222
240
|
let total_files = context.coverage.coverage_per_stylesheet.length;
|
|
223
241
|
let total_checks = total_files + 1;
|
|
224
242
|
let checks_added = 1;
|
|
@@ -254,7 +272,6 @@ function print$1({ report, context }, params) {
|
|
|
254
272
|
}
|
|
255
273
|
}
|
|
256
274
|
}
|
|
257
|
-
|
|
258
275
|
//#endregion
|
|
259
276
|
//#region src/cli/reporters/json.ts
|
|
260
277
|
function prepare({ report, context }, params) {
|
|
@@ -268,7 +285,7 @@ function prepare({ report, context }, params) {
|
|
|
268
285
|
context
|
|
269
286
|
};
|
|
270
287
|
}
|
|
271
|
-
function print
|
|
288
|
+
function print({ report, context }, params) {
|
|
272
289
|
let logger = report.ok ? console.log : console.error;
|
|
273
290
|
let data = prepare({
|
|
274
291
|
context,
|
|
@@ -276,7 +293,6 @@ function print$2({ report, context }, params) {
|
|
|
276
293
|
}, params);
|
|
277
294
|
logger(JSON.stringify(data));
|
|
278
295
|
}
|
|
279
|
-
|
|
280
296
|
//#endregion
|
|
281
297
|
//#region src/cli/help.ts
|
|
282
298
|
function help() {
|
|
@@ -314,12 +330,11 @@ ${styleText("bold", "EXAMPLES")}
|
|
|
314
330
|
$ css-coverage --coverage-dir=./coverage --min-coverage=0.8 --reporter=json
|
|
315
331
|
`.trim();
|
|
316
332
|
}
|
|
317
|
-
|
|
318
333
|
//#endregion
|
|
319
334
|
//#region src/cli/cli.ts
|
|
320
335
|
async function cli(cli_args) {
|
|
321
336
|
if (!cli_args || cli_args.length === 0 || cli_args.includes("--help") || cli_args.includes("-h")) return console.log(help());
|
|
322
|
-
let params =
|
|
337
|
+
let params = parse_arguments(cli_args);
|
|
323
338
|
let coverage_data = await read(params["coverage-dir"]);
|
|
324
339
|
let report = program({
|
|
325
340
|
min_coverage: params["min-coverage"],
|
|
@@ -327,8 +342,8 @@ async function cli(cli_args) {
|
|
|
327
342
|
}, coverage_data);
|
|
328
343
|
if (report.report.ok === false) process.exitCode = 1;
|
|
329
344
|
if (params.reporter === "tap") return print$1(report, params);
|
|
330
|
-
if (params.reporter === "json") return print
|
|
331
|
-
return print(report, params);
|
|
345
|
+
if (params.reporter === "json") return print(report, params);
|
|
346
|
+
return print$2(report, params);
|
|
332
347
|
}
|
|
333
348
|
try {
|
|
334
349
|
await cli(process.argv.slice(2));
|
|
@@ -336,6 +351,5 @@ try {
|
|
|
336
351
|
console.error(error);
|
|
337
352
|
process.exit(1);
|
|
338
353
|
}
|
|
339
|
-
|
|
340
354
|
//#endregion
|
|
341
|
-
export {
|
|
355
|
+
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
|
/**
|
|
@@ -199,12 +219,11 @@ var DOMParser = class {
|
|
|
199
219
|
styles.push({ textContent: text });
|
|
200
220
|
pos = close + 8;
|
|
201
221
|
}
|
|
202
|
-
return { querySelectorAll(
|
|
222
|
+
return { querySelectorAll(_selector) {
|
|
203
223
|
return styles;
|
|
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,62 +1,67 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
2
|
+
"name": "@projectwallace/css-code-coverage",
|
|
3
|
+
"version": "0.9.1",
|
|
4
|
+
"description": "Generate useful CSS Code Coverage report from browser-reported coverage",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"code",
|
|
7
|
+
"coverage",
|
|
8
|
+
"css",
|
|
9
|
+
"dead",
|
|
10
|
+
"styles",
|
|
11
|
+
"testing",
|
|
12
|
+
"unused",
|
|
13
|
+
"used"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/projectwallace/css-code-coverage",
|
|
16
|
+
"license": "EUPL-1.2",
|
|
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"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
29
|
+
"main": "dist/index.js",
|
|
30
|
+
"types": "dist/index.d.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "c8 --reporter=text --reporter=lcov playwright test",
|
|
37
|
+
"build": "tsdown",
|
|
38
|
+
"check": "tsc --noEmit",
|
|
39
|
+
"lint": "oxlint --config .oxlintrc.json; oxfmt --check",
|
|
40
|
+
"lint-package": "publint",
|
|
41
|
+
"knip": "knip"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@projectwallace/css-parser": "^0.13.8",
|
|
45
|
+
"@projectwallace/format-css": "^2.2.6"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@codecov/vite-plugin": "^1.9.1",
|
|
49
|
+
"@playwright/test": "^1.57.0",
|
|
50
|
+
"@projectwallace/preset-oxlint": "^0.0.7",
|
|
51
|
+
"@types/node": "^25.0.3",
|
|
52
|
+
"c8": "^11.0.0",
|
|
53
|
+
"knip": "^5.82.0",
|
|
54
|
+
"oxfmt": "^0.41.0",
|
|
55
|
+
"oxlint": "^1.39.0",
|
|
56
|
+
"publint": "^0.3.16",
|
|
57
|
+
"tsdown": "^0.21.2",
|
|
58
|
+
"typescript": "^5.9.3"
|
|
59
|
+
},
|
|
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"
|
|
62
67
|
}
|