@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.
@@ -1,52 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgs, styleText } from "node:util";
3
- import * as v from "valibot";
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 show_uncovered_options = {
10
- none: "none",
11
- all: "all",
12
- violations: "violations"
13
- };
14
- const reporters = {
15
- pretty: "pretty",
16
- tap: "tap",
17
- json: "json"
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
- return values;
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 v.safeParse(v.array(CoverageSchema), input).success;
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 parse_result = JSON.parse(input);
120
- return is_valid_coverage(parse_result) ? parse_result : [];
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$1) {
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$1("blueBright", name),
158
- styleText$1("magentaBright", prelude),
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$1("cyanBright", css.slice(0, css.indexOf(":"))),
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$1("greenBright", css.slice(0, -2)), "{}"].join("");
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$1("greenBright", css.slice(0, -1)), ","].join("");
172
- return [styleText$1("greenBright", css.slice(0, -1)), "{"].join("");
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: styleText$1, print_width }) {
156
+ function print_lines({ report, context }, params, { styleText, print_width }) {
175
157
  let output = [];
176
- if (report.min_line_coverage.ok) output.push(`${styleText$1(["bold", "green"], "Success")}: total line coverage is ${percentage(report.min_line_coverage.actual)}`);
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$1(["bold", "red"], "Failed")}: line coverage is ${percentage(actual)}% which is lower than the threshold of ${expected}`);
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: ok$1 } = report.min_file_line_coverage;
185
- if (ok$1) output.push(`${styleText$1(["bold", "green"], "Success")}: all files pass minimum line coverage of ${percentage(expected)}`);
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$1(["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)})`);
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$1("dim", "─".repeat(print_width)));
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$1("dim", "─".repeat(print_width)));
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$1) => !chunk$1.is_covered)) {
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$1("dim", line_number(x)),
213
- styleText$1("dim", indent(lines[x - 1]))
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$1("red", "▌"),
217
- styleText$1("dim", line_number(i)),
218
- highlight(indent(lines[i - 1]), styleText$1)
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$1("dim", line_number(y)),
223
- styleText$1("dim", indent(lines[y - 1]))
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$2({ report, context }, params) {
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 --coverage-dir=<dir> --min-coverage=<number> [options]
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 --coverage-dir=./coverage --min-coverage=0.8
326
+ $ css-coverage ./coverage --min-coverage=0.8
346
327
 
347
328
  ${styleText("dim", "# Require 50% coverage per file")}
348
- $ css-coverage --coverage-dir=./coverage --min-coverage=0.8 --min-file-coverage=0.5
329
+ $ css-coverage ./coverage --min-coverage=0.8 --min-file-coverage=0.5
349
330
 
350
331
  ${styleText("dim", "Report JSON")}
351
- $ css-coverage --coverage-dir=./coverage --min-coverage=0.8 --reporter=json
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 = validate_arguments(parse_arguments(cli_args));
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$2(report, params);
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
- declare let RangeSchema: v.ObjectSchema<{
5
- readonly start: v.NumberSchema<undefined>;
6
- readonly end: v.NumberSchema<undefined>;
7
- }, undefined>;
8
- type Range = v.InferInput<typeof RangeSchema>;
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
- start: number;
23
- end: number;
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 * as v from "valibot";
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 v.safeParse(v.array(CoverageSchema), input).success;
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 parse_result = JSON.parse(input);
20
- return is_valid_coverage(parse_result) ? parse_result : [];
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 (WHITESPACE_ONLY_REGEX.test(stylesheet.text.slice(chunk.start_offset, chunk.end_offset)) || chunk.end_offset === chunk.start_offset) {
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 !== stylesheet.text.length - 1) chunks.push({
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 - 1)).trim();
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 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 });
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$1 = text.charAt(next_offset);
300
- while (/\s/.test(next_char$1)) {
318
+ let next_char = text.charAt(next_offset);
319
+ while (/\s/.test(next_char)) {
301
320
  next_offset++;
302
- next_char$1 = text.charAt(next_offset);
321
+ next_char = text.charAt(next_offset);
303
322
  }
304
- if (next_char$1 === "{") range.end = range.end + 1;
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), []).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));
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.9.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
- "dead",
20
- "styles"
13
+ "used"
21
14
  ],
15
+ "homepage": "https://github.com/projectwallace/css-code-coverage",
22
16
  "license": "EUPL-1.2",
23
- "engines": {
24
- "node": ">=20"
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
- "bin": {
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": "^10.1.3",
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.15.8",
57
+ "tsdown": "^0.21.2",
57
58
  "typescript": "^5.9.3"
58
59
  },
59
- "dependencies": {
60
- "@projectwallace/format-css": "^2.2.0",
61
- "valibot": "^1.2.0"
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
  }