From 216de724e893be6f58fa2b966ec27efa892896db Mon Sep 17 00:00:00 2001 From: Bart Veneman Date: Sun, 19 Oct 2025 00:34:02 +0200 Subject: [PATCH 1/7] Add CLI app --- package-lock.json | 19 ++++++++ package.json | 1 + src/cli/arguments.ts | 89 ++++++++++++++++++++++++++++++++++++ src/cli/cli.ts | 35 ++++++++++++++ src/cli/file-reader.ts | 19 ++++++++ src/cli/program.ts | 91 +++++++++++++++++++++++++++++++++++++ src/cli/reporters/pretty.ts | 82 +++++++++++++++++++++++++++++++++ src/cli/reporters/tap.ts | 48 +++++++++++++++++++ 8 files changed, 384 insertions(+) create mode 100644 src/cli/arguments.ts create mode 100644 src/cli/cli.ts create mode 100644 src/cli/file-reader.ts create mode 100644 src/cli/program.ts create mode 100644 src/cli/reporters/pretty.ts create mode 100644 src/cli/reporters/tap.ts diff --git a/package-lock.json b/package-lock.json index 5a40820..8ed3874 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ }, "devDependencies": { "@playwright/test": "^1.56.0", + "@types/node": "^24.8.1", "c8": "^10.1.3", "linkedom": "^0.18.12", "oxlint": "^1.22.0", @@ -1296,6 +1297,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "24.8.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.8.1.tgz", + "integrity": "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.14.0" + } + }, "node_modules/@volar/language-core": { "version": "2.4.23", "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.23.tgz", @@ -3264,6 +3276,13 @@ "dev": true, "license": "ISC" }, + "node_modules/undici-types": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "dev": true, + "license": "MIT" + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", diff --git a/package.json b/package.json index f19348e..696c261 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ }, "devDependencies": { "@playwright/test": "^1.56.0", + "@types/node": "^24.8.1", "c8": "^10.1.3", "linkedom": "^0.18.12", "oxlint": "^1.22.0", diff --git a/src/cli/arguments.ts b/src/cli/arguments.ts new file mode 100644 index 0000000..4ba3a9e --- /dev/null +++ b/src/cli/arguments.ts @@ -0,0 +1,89 @@ +import { parseArgs } from 'node:util' +import * as v from 'valibot' + +const show_uncovered_options = { + none: 'none', + all: 'all', + violations: 'violations', +} as const + +const reporters = { + pretty: 'pretty', + tap: 'tap', +} as const + +let CoverageDirSchema = v.pipe(v.string(), v.nonEmpty()) +// Coerce args string to number and validate that it's between 0 and 1 +let RatioPercentageSchema = v.pipe(v.string(), v.transform(Number), v.number(), v.minValue(0), v.maxValue(1)) +let ShowUncoveredSchema = v.pipe(v.string(), v.enum(show_uncovered_options)) +let ReporterSchema = v.pipe(v.string(), v.enum(reporters)) + +let CliArgumentsSchema = v.object({ + 'coverage-dir': CoverageDirSchema, + 'min-line-coverage': RatioPercentageSchema, + 'min-file-line-coverage': v.optional(RatioPercentageSchema), + 'show-uncovered': v.optional(ShowUncoveredSchema, show_uncovered_options.none), + reporter: v.optional(ReporterSchema, reporters.pretty), +}) + +export type CliArguments = { + 'coverage-dir': string + 'min-line-coverage': number + 'min-file-line-coverage'?: number + 'show-uncovered': keyof typeof show_uncovered_options + reporter: keyof typeof reporters +} + +type ArgumentIssue = { path?: string; message: string } + +export class InvalidArgumentsError extends Error { + readonly issues: ArgumentIssue[] + + constructor(issues: ArgumentIssue[]) { + super() + this.issues = issues + } +} + +export function validate_arguments(args: ReturnType): CliArguments { + let parse_result = v.safeParse(CliArgumentsSchema, args) + + if (!parse_result.success) { + throw new InvalidArgumentsError( + parse_result.issues.map((issue) => ({ + path: issue.path?.map((path) => path.key).join('.'), + message: issue.message, + })), + ) + } + + return parse_result.output +} + +export function parse_arguments(args: string[]) { + let { values } = parseArgs({ + args, + allowPositionals: true, + options: { + 'coverage-dir': { + type: 'string', + }, + 'min-line-coverage': { + type: 'string', + }, + 'min-file-line-coverage': { + type: 'string', + default: '0', + }, + 'show-uncovered': { + type: 'string', + default: 'none', + }, + reporter: { + type: 'string', + default: 'pretty', + }, + }, + }) + return values +} diff --git a/src/cli/cli.ts b/src/cli/cli.ts new file mode 100644 index 0000000..303ec83 --- /dev/null +++ b/src/cli/cli.ts @@ -0,0 +1,35 @@ +import { validate_arguments, parse_arguments, InvalidArgumentsError } from './arguments.ts' +import { program, MissingDataError } from './program.ts' +import { read } from './file-reader.ts' +import { print as pretty } from './reporters/pretty.ts' +import { print as tap } from './reporters/tap.ts' + +async function cli(cli_args: string[]) { + const args = parse_arguments(cli_args) + let params = validate_arguments(args) + let coverage_data = await read(params['coverage-dir']) + let report = program( + { + min_file_coverage: params['min-line-coverage'], + min_file_line_coverage: params['min-file-line-coverage'], + }, + coverage_data, + ) + + if (report.report.ok === false) { + process.exitCode = 1 + } + + if (params.reporter === 'pretty') { + pretty(report, params) + } else if (params.reporter === 'tap') { + tap(report, params) + } +} + +try { + await cli(process.argv.slice(2)) +} catch (error) { + console.error(error) + process.exit(1) +} diff --git a/src/cli/file-reader.ts b/src/cli/file-reader.ts new file mode 100644 index 0000000..532f300 --- /dev/null +++ b/src/cli/file-reader.ts @@ -0,0 +1,19 @@ +import { readFile, stat, readdir } from 'node:fs/promises' +import { join } from 'node:path' +import { parse_coverage, type Coverage } from '../parse-coverage.ts' + +export async function read(coverage_dir: string): Promise { + let s = await stat(coverage_dir) + if (!s.isDirectory()) throw new TypeError('InvalidDirectory') + + let file_paths = await readdir(coverage_dir) + let parsed_files: Coverage[] = [] + + for (let file_path of file_paths) { + if (!file_path.endsWith('.json')) continue + let contents = await readFile(join(coverage_dir, file_path), 'utf-8') + let parsed = parse_coverage(contents) + parsed_files.push(...parsed) + } + return parsed_files +} diff --git a/src/cli/program.ts b/src/cli/program.ts new file mode 100644 index 0000000..b7462fe --- /dev/null +++ b/src/cli/program.ts @@ -0,0 +1,91 @@ +import type { CliArguments } from './arguments' +import { calculate_coverage, type Coverage, type CoverageResult } from '../index.ts' +import { read } from './file-reader.ts' +import { DOMParser } from 'linkedom' + +function parse_html(html: string) { + return new DOMParser().parseFromString(html, 'text/html') +} + +export class MissingDataError extends Error { + constructor() { + super('Missing data to analyze') + } +} + +export type Report = { + context: { + coverage: CoverageResult + } + report: { + ok: boolean + min_line_coverage: { + expected: number + actual: number + ok: boolean + } + min_file_line_coverage: { + expected?: number + actual: number + ok: boolean + } + } +} + +function validate_min_line_coverage(actual: number, expected: number) { + return { + ok: actual >= expected, + actual, + expected, + } +} + +function validate_min_file_line_coverage(actual: number, expected: number | undefined) { + if (expected === undefined) { + return { + ok: true, + actual, + expected, + } + } + + return { + ok: actual >= expected, + actual, + expected, + } +} + +export function program( + { + min_file_coverage, + min_file_line_coverage, + }: { + min_file_coverage: number + min_file_line_coverage?: number + }, + coverage_data: Coverage[], +) { + if (coverage_data.length === 0) { + throw new MissingDataError() + } + let coverage = calculate_coverage(coverage_data, parse_html) + let min_line_coverage_result = validate_min_line_coverage(coverage.line_coverage_ratio, min_file_coverage) + let min_file_line_coverage_result = validate_min_file_line_coverage( + Math.min(...coverage.coverage_per_stylesheet.map((sheet) => sheet.line_coverage_ratio)), + min_file_line_coverage, + ) + + let result: Report = { + context: { + coverage, + }, + report: { + ok: min_line_coverage_result.ok && min_file_line_coverage_result.ok, + min_line_coverage: min_line_coverage_result, + min_file_line_coverage: min_file_line_coverage_result, + }, + } + + return result +} diff --git a/src/cli/reporters/pretty.ts b/src/cli/reporters/pretty.ts new file mode 100644 index 0000000..14547fc --- /dev/null +++ b/src/cli/reporters/pretty.ts @@ -0,0 +1,82 @@ +import { styleText } from 'node:util' +import type { Report } from '../program' +import type { CliArguments } from '../arguments' + +export function print({ report, context }: Report, params: CliArguments) { + if (report.min_line_coverage.ok) { + console.log(`${styleText(['bold', 'green'], 'Success')}: total line coverage is ${(report.min_line_coverage.actual * 100).toFixed(2)}%`) + } else { + console.error( + `${styleText(['bold', 'red'], 'Failed')}: line coverage is ${(report.min_line_coverage.actual * 100).toFixed( + 2, + )}% which is lower than the threshold of ${report.min_line_coverage.expected}`, + ) + } + + if (report.min_file_line_coverage.expected !== undefined) { + let { expected, ok } = report.min_file_line_coverage + if (ok) { + console.log(`${styleText(['bold', 'green'], 'Success')}: all files pass minimum line coverage of ${expected * 100}%`) + } else { + let num_files_failed = context.coverage.coverage_per_stylesheet.filter((sheet) => sheet.line_coverage_ratio < expected!).length + console.error( + `${styleText(['bold', 'red'], 'Failed')}: ${num_files_failed} files do not meet the minimum line coverage of ${expected * 100}%`, + ) + if (params['show-uncovered'] === 'none') { + console.log(` Hint: set --show-uncovered=violations to see which files didn't pass`) + } + } + } + + // Show un-covered chunks + if (params['show-uncovered'] !== 'none') { + const NUM_LEADING_LINES = 3 + const NUM_TRAILING_LINES = NUM_LEADING_LINES + let terminal_width = process.stdout.columns || 80 + let line_number = (num: number, covered: boolean = true) => `${num.toString().padStart(5, ' ')} ${covered ? '│' : '━'} ` + let min_file_line_coverage = report.min_file_line_coverage.expected + + 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 !== undefined && + min_file_line_coverage !== 0 && + sheet.line_coverage_ratio < min_file_line_coverage && + params['show-uncovered'] === 'violations') + ) { + console.log() + console.log(styleText('dim', '─'.repeat(terminal_width))) + console.log(sheet.url) + console.log(`Coverage: ${(sheet.line_coverage_ratio * 100).toFixed(2)}%, ${sheet.covered_lines}/${sheet.total_lines} lines covered`) + if (min_file_line_coverage && min_file_line_coverage !== 0 && sheet.line_coverage_ratio < min_file_line_coverage) { + let lines_to_cover = min_file_line_coverage * sheet.total_lines - sheet.covered_lines + console.log(`💡 Cover ${Math.ceil(lines_to_cover)} more lines to meet the file threshold of ${min_file_line_coverage * 100}%`) + } + console.log(styleText('dim', '─'.repeat(terminal_width))) + + let lines = sheet.text.split('\n') + let line_coverage = sheet.line_coverage + + for (let i = 0; i < lines.length; i++) { + if (line_coverage[i] === 0) { + // Rewind cursor N lines to render N previous lines + for (let j = i - NUM_LEADING_LINES; j < i; j++) { + console.log(styleText('dim', line_number(j)), styleText('dim', lines[j]!)) + } + // Render uncovered lines while increasing cursor until reaching next covered block + while (line_coverage[i] === 0) { + console.log(styleText('red', line_number(i, false)), lines[i]) + i++ + } + // Forward cursor N lines to render N trailing lines + for (let end = i + NUM_TRAILING_LINES; i < end && i < lines.length; i++) { + console.log(styleText('dim', line_number(i)), styleText('dim', lines[i]!)) + } + // Show empty line between blocks + console.log() + } + } + } + } + } +} diff --git a/src/cli/reporters/tap.ts b/src/cli/reporters/tap.ts new file mode 100644 index 0000000..d90c9bf --- /dev/null +++ b/src/cli/reporters/tap.ts @@ -0,0 +1,48 @@ +import type { CliArguments } from '../arguments' +import type { Report } from '../program' + +export function print({ report, context }: Report, params: CliArguments) { + let total_files = context.coverage.coverage_per_stylesheet.length + let total_checks = total_files + 1 + let checks_added = 1 + + if (report.min_file_line_coverage.expected !== undefined) { + total_checks++ + checks_added++ + } + + console.log('TAP version 13') + console.log(`1..${total_checks}`) + + // global line coverage + if (report.min_line_coverage.ok) { + console.log(`ok 1 - overall line coverage`) + } else { + console.log(`not ok 1 - overall line coverage`) + } + + // per-file line coverage + if (report.min_file_line_coverage.expected !== undefined) { + if (report.min_file_line_coverage.ok) { + console.log(`ok 2 - line coverage per file`) + } else { + console.log(`not ok 2 - line coverage per file`) + } + + for (let i = 0; i < total_files; i++) { + let sheet = context.coverage.coverage_per_stylesheet[i]! + let num = i + checks_added + 1 + if (sheet.line_coverage_ratio < report.min_file_line_coverage.expected) { + console.log(`not ok ${num} - ${sheet.url}`) + console.log('---') + console.log(`expected_coverage: ${(report.min_file_line_coverage.expected * 100).toFixed(2)}%`) + console.log(`actual_coverage: ${(sheet.line_coverage_ratio * 100).toFixed(2)}%`) + console.log(`lines_covered: ${sheet.covered_lines}`) + console.log(`total_lines: ${sheet.total_lines}`) + console.log('...') + } else { + console.log(`ok ${num} - ${sheet.url}`) + } + } + } +} From 0529f0cc4df0a4869cd5e79c4db39e3b6f502db8 Mon Sep 17 00:00:00 2001 From: Bart Veneman Date: Sun, 19 Oct 2025 09:42:04 +0200 Subject: [PATCH 2/7] tweaks --- src/cli/reporters/pretty.ts | 2 +- src/decuplicate.ts | 4 ++-- src/deduplicate.test.ts | 38 ++++++++++++++++--------------------- src/index.ts | 8 ++++---- src/prettify.ts | 19 ++++++++++--------- 5 files changed, 33 insertions(+), 38 deletions(-) diff --git a/src/cli/reporters/pretty.ts b/src/cli/reporters/pretty.ts index 14547fc..c8d1f78 100644 --- a/src/cli/reporters/pretty.ts +++ b/src/cli/reporters/pretty.ts @@ -61,7 +61,7 @@ export function print({ report, context }: Report, params: CliArguments) { if (line_coverage[i] === 0) { // Rewind cursor N lines to render N previous lines for (let j = i - NUM_LEADING_LINES; j < i; j++) { - console.log(styleText('dim', line_number(j)), styleText('dim', lines[j]!)) + console.log(styleText('dim', line_number(j)), styleText('dim', lines[j] || '')) } // Render uncovered lines while increasing cursor until reaching next covered block while (line_coverage[i] === 0) { diff --git a/src/decuplicate.ts b/src/decuplicate.ts index bd788fd..9a97c6f 100644 --- a/src/decuplicate.ts +++ b/src/decuplicate.ts @@ -6,7 +6,7 @@ import type { Coverage, Range } from './parse-coverage.ts' * - if a duplicate stylesheet enters the room, we add it's ranges to the existing stylesheet's ranges * - only bytes of deduplicated stylesheets are counted */ -export function deduplicate_entries(entries: Coverage[]): Map> { +export function deduplicate_entries(entries: Coverage[]): Coverage[] { let checked_stylesheets = new Map() for (let entry of entries) { @@ -36,5 +36,5 @@ export function deduplicate_entries(entries: Coverage[]): Map ({ text, url, ranges })) } diff --git a/src/deduplicate.test.ts b/src/deduplicate.test.ts index 908324a..77190c6 100644 --- a/src/deduplicate.test.ts +++ b/src/deduplicate.test.ts @@ -7,16 +7,16 @@ test('handles a single entry', () => { ranges: [{ start: 0, end: 4 }], url: 'example.com', } - expect(deduplicate_entries([entry])).toEqual(new Map([[entry.text, { url: entry.url, ranges: entry.ranges }]])) + expect(deduplicate_entries([entry])).toEqual([entry]) }) -test('deduplicats a simple duplicate entry', () => { +test('deduplicates a simple duplicate entry', () => { let entry = { text: 'a {}', ranges: [{ start: 0, end: 4 }], url: 'example.com', } - expect(deduplicate_entries([entry, entry])).toEqual(new Map([[entry.text, { url: entry.url, ranges: entry.ranges }]])) + expect(deduplicate_entries([entry, entry])).toEqual([entry]) }) test('merges two identical texts with different URLs and identical ranges', () => { @@ -33,7 +33,7 @@ test('merges two identical texts with different URLs and identical ranges', () = }, ] let first = entries.at(0)! - expect(deduplicate_entries(entries)).toEqual(new Map([[first.text, { url: first.url, ranges: first.ranges }]])) + expect(deduplicate_entries(entries)).toEqual([{ text: first.text, url: first.url, ranges: first.ranges }]) }) test('merges different ranges on identical CSS, different URLs', () => { @@ -50,9 +50,7 @@ test('merges different ranges on identical CSS, different URLs', () => { }, ] let first = entries.at(0)! - expect(deduplicate_entries(entries)).toEqual( - new Map([[first.text, { url: first.url, ranges: [first.ranges[0], entries[1]!.ranges[0]] }]]), - ) + expect(deduplicate_entries(entries)).toEqual([{ text: first.text, url: first.url, ranges: [first.ranges[0], entries[1]!.ranges[0]] }]) }) test('merges different ranges on identical CSS, identical URLs', () => { @@ -68,9 +66,9 @@ test('merges different ranges on identical CSS, identical URLs', () => { url: 'example.com', }, ] - expect(deduplicate_entries(entries)).toEqual( - new Map([[entries[0]!.text, { url: entries[0]!.url, ranges: [entries[0]!.ranges[0], entries[1]!.ranges[0]] }]]), - ) + expect(deduplicate_entries(entries)).toEqual([ + { text: entries[0]!.text, url: entries[0]!.url, ranges: [entries[0]!.ranges[0], entries[1]!.ranges[0]] }, + ]) }) test('does not merge different CSS with different URLs and identical ranges', () => { @@ -86,12 +84,10 @@ test('does not merge different CSS with different URLs and identical ranges', () url: 'example.com/b', }, ] - expect(deduplicate_entries(entries)).toEqual( - new Map([ - [entries[0]!.text, { url: entries[0]!.url, ranges: entries[0]!.ranges }], - [entries[1]!.text, { url: entries[1]!.url, ranges: entries[1]!.ranges }], - ]), - ) + expect(deduplicate_entries(entries)).toEqual([ + { text: entries[0]!.text, url: entries[0]!.url, ranges: entries[0]!.ranges }, + { text: entries[1]!.text, url: entries[1]!.url, ranges: entries[1]!.ranges }, + ]) }) test('does not merge different CSS with same URLs and identical ranges', () => { @@ -107,10 +103,8 @@ test('does not merge different CSS with same URLs and identical ranges', () => { url: 'example.com', }, ] - expect(deduplicate_entries(entries)).toEqual( - new Map([ - [entries[0]!.text, { url: entries[0]!.url, ranges: entries[0]!.ranges }], - [entries[1]!.text, { url: entries[1]!.url, ranges: entries[1]!.ranges }], - ]), - ) + expect(deduplicate_entries(entries)).toEqual([ + { text: entries[0]!.text, url: entries[0]!.url, ranges: entries[0]!.ranges }, + { text: entries[1]!.text, url: entries[1]!.url, ranges: entries[1]!.ranges }, + ]) }) diff --git a/src/index.ts b/src/index.ts index ed1a932..78d8158 100644 --- a/src/index.ts +++ b/src/index.ts @@ -57,12 +57,12 @@ export function calculate_coverage(coverage: Coverage[], parse_html?: Parser): C throw new TypeError('No valid coverage data found') } - let filtered_coverage = filter_coverage(coverage, parse_html) - let prettified_coverage = prettify(filtered_coverage) - let deduplicated = deduplicate_entries(prettified_coverage) + let filtered_coverage: Coverage[] = filter_coverage(coverage, parse_html) + let prettified_coverage: Coverage[] = prettify(filtered_coverage) + let deduplicated: Coverage[] = deduplicate_entries(prettified_coverage) // Calculate coverage for each individual stylesheet we found - let coverage_per_stylesheet = Array.from(deduplicated).map(([text, { url, ranges }]) => { + let coverage_per_stylesheet = deduplicated.map(({ text, url, ranges }) => { function is_line_covered(line: string, start_offset: number) { let end = start_offset + line.length let next_offset = end + 1 // account for newline character diff --git a/src/prettify.ts b/src/prettify.ts index 9f752ac..65505de 100644 --- a/src/prettify.ts +++ b/src/prettify.ts @@ -3,18 +3,19 @@ import type { Range, Coverage } from './parse-coverage.ts' // css-tree tokens: https://github.com/csstree/csstree/blob/be5ea1257009960c04cccdb58bb327263e27e3b3/lib/tokenizer/types.js import { tokenize, tokenTypes } from 'css-tree/tokenizer' +let irrelevant_tokens: Set = new Set([ + tokenTypes.EOF, + tokenTypes.BadString, + tokenTypes.BadUrl, + tokenTypes.WhiteSpace, + tokenTypes.Semicolon, + tokenTypes.Comment, + tokenTypes.Colon, +]) + export function prettify(coverage: Coverage[]): Coverage[] { return coverage.map(({ url, text, ranges }) => { let formatted = format(text) - let irrelevant_tokens: Set = new Set([ - tokenTypes.EOF, - tokenTypes.BadString, - tokenTypes.BadUrl, - tokenTypes.WhiteSpace, - tokenTypes.Semicolon, - tokenTypes.Comment, - tokenTypes.Colon, - ]) // Initialize the ranges with an empty array of token indexes let ext_ranges: (Range & { tokens: number[] })[] = ranges.map(({ start, end }) => ({ start, end, tokens: [] })) From 58cf8cddb2cff9ea3a7590f8249bae0c9f5ef303 Mon Sep 17 00:00:00 2001 From: Bart Veneman Date: Sun, 19 Oct 2025 11:08:08 +0200 Subject: [PATCH 3/7] move lib to folder --- package.json | 5 +- src/cli/cli.ts | 2 + src/cli/file-reader.ts | 2 +- src/cli/program.ts | 6 +- src/cli/reporters/tap.ts | 56 ++++++++++++++----- src/{ => lib}/decuplicate.ts | 0 src/{ => lib}/deduplicate.test.ts | 0 src/{ => lib}/ext.test.ts | 0 src/{ => lib}/ext.ts | 0 src/{ => lib}/filter-entries.test.ts | 0 src/{ => lib}/filter-entries.ts | 0 src/{ => lib}/index.test.ts | 2 +- src/{ => lib}/index.ts | 0 src/{ => lib}/kitchen-sink.test.ts | 0 src/{ => lib}/parse-coverage.test.ts | 0 src/{ => lib}/parse-coverage.ts | 0 src/{ => lib}/prettify.test.ts | 0 src/{ => lib}/prettify.ts | 0 src/{ => lib}/remap-html.test.ts | 0 src/{ => lib}/remap-html.ts | 0 .../lib/test}/generate-coverage.test.ts | 0 {test => src/lib/test}/generate-coverage.ts | 2 +- src/{ => lib}/types.ts | 0 vite.config.js | 28 +++++----- 24 files changed, 66 insertions(+), 37 deletions(-) rename src/{ => lib}/decuplicate.ts (100%) rename src/{ => lib}/deduplicate.test.ts (100%) rename src/{ => lib}/ext.test.ts (100%) rename src/{ => lib}/ext.ts (100%) rename src/{ => lib}/filter-entries.test.ts (100%) rename src/{ => lib}/filter-entries.ts (100%) rename src/{ => lib}/index.test.ts (99%) rename src/{ => lib}/index.ts (100%) rename src/{ => lib}/kitchen-sink.test.ts (100%) rename src/{ => lib}/parse-coverage.test.ts (100%) rename src/{ => lib}/parse-coverage.ts (100%) rename src/{ => lib}/prettify.test.ts (100%) rename src/{ => lib}/prettify.ts (100%) rename src/{ => lib}/remap-html.test.ts (100%) rename src/{ => lib}/remap-html.ts (100%) rename {test => src/lib/test}/generate-coverage.test.ts (100%) rename {test => src/lib/test}/generate-coverage.ts (96%) rename src/{ => lib}/types.ts (100%) diff --git a/package.json b/package.json index 696c261..8ec98f5 100644 --- a/package.json +++ b/package.json @@ -21,11 +21,12 @@ ], "license": "EUPL-1.2", "engines": { - "node": ">=18.0.0" + "node": ">=22.18.0" }, "type": "module", "files": [ - "dist" + "dist", + "src/cli" ], "main": "dist/css-code-coverage.js", "exports": { diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 303ec83..b1ac4bd 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -1,3 +1,5 @@ +#!/usr/bin/env node + import { validate_arguments, parse_arguments, InvalidArgumentsError } from './arguments.ts' import { program, MissingDataError } from './program.ts' import { read } from './file-reader.ts' diff --git a/src/cli/file-reader.ts b/src/cli/file-reader.ts index 532f300..22a4faf 100644 --- a/src/cli/file-reader.ts +++ b/src/cli/file-reader.ts @@ -1,6 +1,6 @@ import { readFile, stat, readdir } from 'node:fs/promises' import { join } from 'node:path' -import { parse_coverage, type Coverage } from '../parse-coverage.ts' +import { parse_coverage, type Coverage } from '../lib/parse-coverage.ts' export async function read(coverage_dir: string): Promise { let s = await stat(coverage_dir) diff --git a/src/cli/program.ts b/src/cli/program.ts index b7462fe..d8b4748 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -1,6 +1,4 @@ -import type { CliArguments } from './arguments' -import { calculate_coverage, type Coverage, type CoverageResult } from '../index.ts' -import { read } from './file-reader.ts' +import { calculate_coverage, type Coverage, type CoverageResult } from '../lib/index.ts' import { DOMParser } from 'linkedom' function parse_html(html: string) { @@ -9,7 +7,7 @@ function parse_html(html: string) { export class MissingDataError extends Error { constructor() { - super('Missing data to analyze') + super('No data to analyze') } } diff --git a/src/cli/reporters/tap.ts b/src/cli/reporters/tap.ts index d90c9bf..50696bd 100644 --- a/src/cli/reporters/tap.ts +++ b/src/cli/reporters/tap.ts @@ -1,6 +1,30 @@ import type { CliArguments } from '../arguments' import type { Report } from '../program' +function version() { + console.log('TAP version 13') +} + +function plan(total: number) { + console.log(`1..${total}`) +} + +function ok(n: number, description?: string) { + console.log(`ok ${n} ${description ? `- ${description}` : ''}`) +} + +function not_ok(n: number, description?: string) { + console.log(`not ok ${n} ${description ? `- ${description}` : ''}`) +} + +function meta(data: Record) { + console.log(' ---') + for (let key in data) { + console.log(` ${key}: ${data[key]}`) + } + console.log(' ...') +} + export function print({ report, context }: Report, params: CliArguments) { let total_files = context.coverage.coverage_per_stylesheet.length let total_checks = total_files + 1 @@ -11,37 +35,41 @@ export function print({ report, context }: Report, params: CliArguments) { checks_added++ } - console.log('TAP version 13') - console.log(`1..${total_checks}`) + version() + plan(total_checks) // global line coverage if (report.min_line_coverage.ok) { - console.log(`ok 1 - overall line coverage`) + ok(1, 'overall line coverage') } else { - console.log(`not ok 1 - overall line coverage`) + not_ok(1, 'overall line coverage') } // per-file line coverage if (report.min_file_line_coverage.expected !== undefined) { if (report.min_file_line_coverage.ok) { - console.log(`ok 2 - line coverage per file`) + ok(2, 'line coverage per file') } else { - console.log(`not ok 2 - line coverage per file`) + not_ok(2, 'line coverage per file') + meta({ + expected_min_coverage: report.min_file_line_coverage.expected, + actual_min_coverage: report.min_file_line_coverage.actual, + }) } for (let i = 0; i < total_files; i++) { let sheet = context.coverage.coverage_per_stylesheet[i]! let num = i + checks_added + 1 if (sheet.line_coverage_ratio < report.min_file_line_coverage.expected) { - console.log(`not ok ${num} - ${sheet.url}`) - console.log('---') - console.log(`expected_coverage: ${(report.min_file_line_coverage.expected * 100).toFixed(2)}%`) - console.log(`actual_coverage: ${(sheet.line_coverage_ratio * 100).toFixed(2)}%`) - console.log(`lines_covered: ${sheet.covered_lines}`) - console.log(`total_lines: ${sheet.total_lines}`) - console.log('...') + not_ok(num, sheet.url) + meta({ + expected_coverage: report.min_file_line_coverage.expected, + actual_coverage: report.min_file_line_coverage.actual, + lines_covered: sheet.covered_lines, + total_lines: sheet.total_lines, + }) } else { - console.log(`ok ${num} - ${sheet.url}`) + ok(num, sheet.url) } } } diff --git a/src/decuplicate.ts b/src/lib/decuplicate.ts similarity index 100% rename from src/decuplicate.ts rename to src/lib/decuplicate.ts diff --git a/src/deduplicate.test.ts b/src/lib/deduplicate.test.ts similarity index 100% rename from src/deduplicate.test.ts rename to src/lib/deduplicate.test.ts diff --git a/src/ext.test.ts b/src/lib/ext.test.ts similarity index 100% rename from src/ext.test.ts rename to src/lib/ext.test.ts diff --git a/src/ext.ts b/src/lib/ext.ts similarity index 100% rename from src/ext.ts rename to src/lib/ext.ts diff --git a/src/filter-entries.test.ts b/src/lib/filter-entries.test.ts similarity index 100% rename from src/filter-entries.test.ts rename to src/lib/filter-entries.test.ts diff --git a/src/filter-entries.ts b/src/lib/filter-entries.ts similarity index 100% rename from src/filter-entries.ts rename to src/lib/filter-entries.ts diff --git a/src/index.test.ts b/src/lib/index.test.ts similarity index 99% rename from src/index.test.ts rename to src/lib/index.test.ts index 3d74b01..21af9dd 100644 --- a/src/index.test.ts +++ b/src/lib/index.test.ts @@ -1,5 +1,5 @@ import { test, expect } from '@playwright/test' -import { generate_coverage } from '../test/generate-coverage.ts' +import { generate_coverage } from './test/generate-coverage.ts' import { calculate_coverage } from './index.ts' import type { Coverage } from './parse-coverage.ts' import { format } from '@projectwallace/format-css' diff --git a/src/index.ts b/src/lib/index.ts similarity index 100% rename from src/index.ts rename to src/lib/index.ts diff --git a/src/kitchen-sink.test.ts b/src/lib/kitchen-sink.test.ts similarity index 100% rename from src/kitchen-sink.test.ts rename to src/lib/kitchen-sink.test.ts diff --git a/src/parse-coverage.test.ts b/src/lib/parse-coverage.test.ts similarity index 100% rename from src/parse-coverage.test.ts rename to src/lib/parse-coverage.test.ts diff --git a/src/parse-coverage.ts b/src/lib/parse-coverage.ts similarity index 100% rename from src/parse-coverage.ts rename to src/lib/parse-coverage.ts diff --git a/src/prettify.test.ts b/src/lib/prettify.test.ts similarity index 100% rename from src/prettify.test.ts rename to src/lib/prettify.test.ts diff --git a/src/prettify.ts b/src/lib/prettify.ts similarity index 100% rename from src/prettify.ts rename to src/lib/prettify.ts diff --git a/src/remap-html.test.ts b/src/lib/remap-html.test.ts similarity index 100% rename from src/remap-html.test.ts rename to src/lib/remap-html.test.ts diff --git a/src/remap-html.ts b/src/lib/remap-html.ts similarity index 100% rename from src/remap-html.ts rename to src/lib/remap-html.ts diff --git a/test/generate-coverage.test.ts b/src/lib/test/generate-coverage.test.ts similarity index 100% rename from test/generate-coverage.test.ts rename to src/lib/test/generate-coverage.test.ts diff --git a/test/generate-coverage.ts b/src/lib/test/generate-coverage.ts similarity index 96% rename from test/generate-coverage.ts rename to src/lib/test/generate-coverage.ts index 32f84cb..aca54f2 100644 --- a/test/generate-coverage.ts +++ b/src/lib/test/generate-coverage.ts @@ -14,7 +14,7 @@ export async function generate_coverage(html: string, { link_css }: { link_css?: route.fulfill({ status: 200, contentType: 'text/css', - body: link_css || '', + body: link_css, }) }) await page.coverage.startCSSCoverage() diff --git a/src/types.ts b/src/lib/types.ts similarity index 100% rename from src/types.ts rename to src/lib/types.ts diff --git a/vite.config.js b/vite.config.js index 9e82578..1708000 100644 --- a/vite.config.js +++ b/vite.config.js @@ -3,19 +3,19 @@ import { defineConfig } from 'vite' import dts from 'vite-plugin-dts' import pkg from './package.json' -export default defineConfig({ - build: { - lib: { - entry: resolve(__dirname, 'src/index.ts'), - formats: ['es'], +export default defineConfig(({ mode }) => { + return { + build: { + outDir: 'dist/lib', + lib: { + entry: resolve(__dirname, 'src/lib/index.ts'), + formats: ['es'], + }, + rollupOptions: { + external: Object.keys(pkg.dependencies).concat('css-tree/tokenizer'), + }, + sourcemap: true, }, - rollupOptions: { - external: Object.keys(pkg.dependencies).concat('css-tree/tokenizer'), - }, - }, - plugins: [ - dts({ - insertTypesEntry: true, - }), - ], + plugins: [dts()], + } }) From 1c6773e491c3b79e8cbf4455c5b4cdab05d9f52d Mon Sep 17 00:00:00 2001 From: Bart Veneman Date: Sun, 19 Oct 2025 11:46:57 +0200 Subject: [PATCH 4/7] fixes --- src/cli/arguments.ts | 4 +-- src/cli/reporters/pretty.ts | 51 ++++++++++++++++++++++++------------- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/cli/arguments.ts b/src/cli/arguments.ts index 4ba3a9e..af20d2f 100644 --- a/src/cli/arguments.ts +++ b/src/cli/arguments.ts @@ -22,7 +22,7 @@ let CliArgumentsSchema = v.object({ 'coverage-dir': CoverageDirSchema, 'min-line-coverage': RatioPercentageSchema, 'min-file-line-coverage': v.optional(RatioPercentageSchema), - 'show-uncovered': v.optional(ShowUncoveredSchema, show_uncovered_options.none), + 'show-uncovered': v.optional(ShowUncoveredSchema, show_uncovered_options.violations), reporter: v.optional(ReporterSchema, reporters.pretty), }) @@ -77,7 +77,7 @@ export function parse_arguments(args: string[]) { }, 'show-uncovered': { type: 'string', - default: 'none', + default: 'violations', }, reporter: { type: 'string', diff --git a/src/cli/reporters/pretty.ts b/src/cli/reporters/pretty.ts index c8d1f78..98ee3f9 100644 --- a/src/cli/reporters/pretty.ts +++ b/src/cli/reporters/pretty.ts @@ -1,7 +1,13 @@ +// oxlint-disable max-depth import { styleText } from 'node:util' import type { Report } from '../program' import type { CliArguments } from '../arguments' +// Re-indent because tabs in the terminal tend to be bigger than usual +function indent(line?: string): string { + return (line || '').replace(/^\t+/, (tabs) => ' '.repeat(tabs.length * 4)) +} + export function print({ report, context }: Report, params: CliArguments) { if (report.min_line_coverage.ok) { console.log(`${styleText(['bold', 'green'], 'Success')}: total line coverage is ${(report.min_line_coverage.actual * 100).toFixed(2)}%`) @@ -14,13 +20,15 @@ export function print({ report, context }: Report, params: CliArguments) { } if (report.min_file_line_coverage.expected !== undefined) { - let { expected, ok } = report.min_file_line_coverage + let { expected, actual, ok } = report.min_file_line_coverage if (ok) { console.log(`${styleText(['bold', 'green'], 'Success')}: all files pass minimum line coverage of ${expected * 100}%`) } else { let num_files_failed = context.coverage.coverage_per_stylesheet.filter((sheet) => sheet.line_coverage_ratio < expected!).length console.error( - `${styleText(['bold', 'red'], 'Failed')}: ${num_files_failed} files do not meet the minimum line coverage of ${expected * 100}%`, + `${styleText(['bold', 'red'], 'Failed')}: ${num_files_failed} files do not meet the minimum line coverage of ${ + expected * 100 + }% (minimum coverage was ${(actual * 100).toFixed(2)}%)`, ) if (params['show-uncovered'] === 'none') { console.log(` Hint: set --show-uncovered=violations to see which files didn't pass`) @@ -48,9 +56,10 @@ export function print({ report, context }: Report, params: CliArguments) { console.log(styleText('dim', '─'.repeat(terminal_width))) console.log(sheet.url) console.log(`Coverage: ${(sheet.line_coverage_ratio * 100).toFixed(2)}%, ${sheet.covered_lines}/${sheet.total_lines} lines covered`) + if (min_file_line_coverage && min_file_line_coverage !== 0 && sheet.line_coverage_ratio < min_file_line_coverage) { let lines_to_cover = min_file_line_coverage * sheet.total_lines - sheet.covered_lines - console.log(`💡 Cover ${Math.ceil(lines_to_cover)} more lines to meet the file threshold of ${min_file_line_coverage * 100}%`) + console.log(`Tip: cover ${Math.ceil(lines_to_cover)} more lines to meet the file threshold of ${min_file_line_coverage * 100}%`) } console.log(styleText('dim', '─'.repeat(terminal_width))) @@ -58,23 +67,29 @@ export function print({ report, context }: Report, params: CliArguments) { let line_coverage = sheet.line_coverage for (let i = 0; i < lines.length; i++) { - if (line_coverage[i] === 0) { - // Rewind cursor N lines to render N previous lines - for (let j = i - NUM_LEADING_LINES; j < i; j++) { - console.log(styleText('dim', line_number(j)), styleText('dim', lines[j] || '')) - } - // Render uncovered lines while increasing cursor until reaching next covered block - while (line_coverage[i] === 0) { - console.log(styleText('red', line_number(i, false)), lines[i]) - i++ - } - // Forward cursor N lines to render N trailing lines - for (let end = i + NUM_TRAILING_LINES; i < end && i < lines.length; i++) { - console.log(styleText('dim', line_number(i)), styleText('dim', lines[i]!)) + if (line_coverage[i] === 1) continue + + // Rewind cursor N lines to render N previous lines + for (let j = i - NUM_LEADING_LINES; j < i; j++) { + // Make sure that we don't try to start before line 0 + if (j >= 0) { + console.log(styleText('dim', line_number(j)), styleText('dim', indent(lines[j]))) } - // Show empty line between blocks - console.log() } + + // Render uncovered lines while increasing cursor until reaching next covered block + while (line_coverage[i] === 0) { + console.log(styleText('red', line_number(i, false)), indent(lines[i])) + i++ + } + + // Forward cursor N lines to render N trailing lines + for (let end = i + NUM_TRAILING_LINES; i < end && i < lines.length; i++) { + console.log(styleText('dim', line_number(i)), styleText('dim', indent(lines[i]))) + } + + // Show empty line between blocks + console.log() } } } From 08d21b3d279a136c13fa9d3acca21cd6db0803d9 Mon Sep 17 00:00:00 2001 From: Bart Veneman Date: Sun, 19 Oct 2025 12:23:35 +0200 Subject: [PATCH 5/7] bundle stuff with tsc, fix exports --- package.json | 16 +++++++++------- src/cli/cli.ts | 10 +++++----- src/cli/file-reader.ts | 2 +- src/cli/program.ts | 2 +- src/cli/reporters/pretty.ts | 4 ++-- src/cli/reporters/tap.ts | 4 ++-- src/{ => lib}/css-tree.d.ts | 0 src/lib/decuplicate.ts | 2 +- src/lib/deduplicate.test.ts | 2 +- src/lib/ext.test.ts | 2 +- src/lib/filter-entries.test.ts | 2 +- src/lib/filter-entries.ts | 8 ++++---- src/lib/index.test.ts | 6 +++--- src/lib/index.ts | 20 ++++++++++---------- src/lib/kitchen-sink.test.ts | 2 +- src/lib/parse-coverage.test.ts | 2 +- src/lib/prettify.test.ts | 2 +- src/lib/prettify.ts | 2 +- src/lib/remap-html.test.ts | 2 +- src/lib/remap-html.ts | 4 ++-- src/lib/test/generate-coverage.test.ts | 2 +- tsconfig.json | 14 +++++++------- 22 files changed, 56 insertions(+), 54 deletions(-) rename src/{ => lib}/css-tree.d.ts (100%) diff --git a/package.json b/package.json index 8ec98f5..5578e41 100644 --- a/package.json +++ b/package.json @@ -25,18 +25,20 @@ }, "type": "module", "files": [ - "dist", - "src/cli" + "dist" ], - "main": "dist/css-code-coverage.js", + "bin": { + "css-coverage": "dist/cli/cli.js" + }, + "main": "dist/lib/index.js", "exports": { - "types": "./dist/index.d.ts", - "default": "./dist/css-code-coverage.js" + "types": "./dist/lib/index.d.ts", + "default": "./dist/lib/index.js" }, - "types": "dist/index.d.ts", + "types": "dist/lib/index.d.ts", "scripts": { "test": "c8 --reporter=text playwright test", - "build": "vite build", + "build": "tsc", "check": "tsc --noEmit", "lint": "oxlint --config .oxlintrc.json", "lint-package": "publint" diff --git a/src/cli/cli.ts b/src/cli/cli.ts index b1ac4bd..227379a 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -1,10 +1,10 @@ #!/usr/bin/env node -import { validate_arguments, parse_arguments, InvalidArgumentsError } from './arguments.ts' -import { program, MissingDataError } from './program.ts' -import { read } from './file-reader.ts' -import { print as pretty } from './reporters/pretty.ts' -import { print as tap } from './reporters/tap.ts' +import { validate_arguments, parse_arguments, InvalidArgumentsError } from './arguments.js' +import { program, MissingDataError } from './program.js' +import { read } from './file-reader.js' +import { print as pretty } from './reporters/pretty.js' +import { print as tap } from './reporters/tap.js' async function cli(cli_args: string[]) { const args = parse_arguments(cli_args) diff --git a/src/cli/file-reader.ts b/src/cli/file-reader.ts index 22a4faf..d52d13d 100644 --- a/src/cli/file-reader.ts +++ b/src/cli/file-reader.ts @@ -1,6 +1,6 @@ import { readFile, stat, readdir } from 'node:fs/promises' import { join } from 'node:path' -import { parse_coverage, type Coverage } from '../lib/parse-coverage.ts' +import { parse_coverage, type Coverage } from '../lib/parse-coverage.js' export async function read(coverage_dir: string): Promise { let s = await stat(coverage_dir) diff --git a/src/cli/program.ts b/src/cli/program.ts index d8b4748..93e0727 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -1,4 +1,4 @@ -import { calculate_coverage, type Coverage, type CoverageResult } from '../lib/index.ts' +import { calculate_coverage, type Coverage, type CoverageResult } from '../lib/index.js' import { DOMParser } from 'linkedom' function parse_html(html: string) { diff --git a/src/cli/reporters/pretty.ts b/src/cli/reporters/pretty.ts index 98ee3f9..b998e4b 100644 --- a/src/cli/reporters/pretty.ts +++ b/src/cli/reporters/pretty.ts @@ -1,7 +1,7 @@ // oxlint-disable max-depth import { styleText } from 'node:util' -import type { Report } from '../program' -import type { CliArguments } from '../arguments' +import type { Report } from '../program.js' +import type { CliArguments } from '../arguments.js' // Re-indent because tabs in the terminal tend to be bigger than usual function indent(line?: string): string { diff --git a/src/cli/reporters/tap.ts b/src/cli/reporters/tap.ts index 50696bd..6bfe135 100644 --- a/src/cli/reporters/tap.ts +++ b/src/cli/reporters/tap.ts @@ -1,5 +1,5 @@ -import type { CliArguments } from '../arguments' -import type { Report } from '../program' +import type { CliArguments } from '../arguments.js' +import type { Report } from '../program.js' function version() { console.log('TAP version 13') diff --git a/src/css-tree.d.ts b/src/lib/css-tree.d.ts similarity index 100% rename from src/css-tree.d.ts rename to src/lib/css-tree.d.ts diff --git a/src/lib/decuplicate.ts b/src/lib/decuplicate.ts index 9a97c6f..42260c1 100644 --- a/src/lib/decuplicate.ts +++ b/src/lib/decuplicate.ts @@ -1,4 +1,4 @@ -import type { Coverage, Range } from './parse-coverage.ts' +import type { Coverage, Range } from './parse-coverage.js' /** * @description * prerequisites diff --git a/src/lib/deduplicate.test.ts b/src/lib/deduplicate.test.ts index 77190c6..f59c4d0 100644 --- a/src/lib/deduplicate.test.ts +++ b/src/lib/deduplicate.test.ts @@ -1,5 +1,5 @@ import { test, expect } from '@playwright/test' -import { deduplicate_entries } from './decuplicate.ts' +import { deduplicate_entries } from './decuplicate.js' test('handles a single entry', () => { let entry = { diff --git a/src/lib/ext.test.ts b/src/lib/ext.test.ts index 5273b43..7ce83ea 100644 --- a/src/lib/ext.test.ts +++ b/src/lib/ext.test.ts @@ -1,5 +1,5 @@ import { test, expect } from '@playwright/test' -import { ext } from './ext.ts' +import { ext } from './ext.js' let valid = [ 'https://example.com/style.css', diff --git a/src/lib/filter-entries.test.ts b/src/lib/filter-entries.test.ts index 3b25228..a71850b 100644 --- a/src/lib/filter-entries.test.ts +++ b/src/lib/filter-entries.test.ts @@ -1,5 +1,5 @@ import { test, expect } from '@playwright/test' -import { filter_coverage } from './filter-entries.ts' +import { filter_coverage } from './filter-entries.js' import { DOMParser } from 'linkedom' function html_parser(html: string) { diff --git a/src/lib/filter-entries.ts b/src/lib/filter-entries.ts index 27e4cc1..a355b69 100644 --- a/src/lib/filter-entries.ts +++ b/src/lib/filter-entries.ts @@ -1,7 +1,7 @@ -import type { Coverage } from './parse-coverage.ts' -import { ext } from './ext.ts' -import type { Parser } from './types.ts' -import { remap_html } from './remap-html.ts' +import type { Coverage } from './parse-coverage.js' +import { ext } from './ext.js' +import type { Parser } from './types.js' +import { remap_html } from './remap-html.js' function is_html(text: string): boolean { return /<\/?(html|body|head|div|span|script|style)/i.test(text) diff --git a/src/lib/index.test.ts b/src/lib/index.test.ts index 21af9dd..da62b6a 100644 --- a/src/lib/index.test.ts +++ b/src/lib/index.test.ts @@ -1,7 +1,7 @@ import { test, expect } from '@playwright/test' -import { generate_coverage } from './test/generate-coverage.ts' -import { calculate_coverage } from './index.ts' -import type { Coverage } from './parse-coverage.ts' +import { generate_coverage } from './test/generate-coverage.js' +import { calculate_coverage } from './index.js' +import type { Coverage } from './parse-coverage.js' import { format } from '@projectwallace/format-css' import { DOMParser } from 'linkedom' diff --git a/src/lib/index.ts b/src/lib/index.ts index 78d8158..9917e39 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,8 +1,8 @@ -import { is_valid_coverage, type Coverage, type Range } from './parse-coverage.ts' -import { prettify } from './prettify.ts' -import { deduplicate_entries } from './decuplicate.ts' -import { filter_coverage } from './filter-entries.ts' -import type { Parser } from './types.ts' +import { is_valid_coverage, type Coverage, type Range } from './parse-coverage.js' +import { prettify } from './prettify.js' +import { deduplicate_entries } from './decuplicate.js' +import { filter_coverage } from './filter-entries.js' +import type { Parser } from './types.js' export type CoverageData = { unused_bytes: number @@ -134,8 +134,8 @@ export function calculate_coverage(coverage: Coverage[], parse_html?: Parser): C ] for (let index = 1; index < line_coverage.length; index++) { - let is_covered = line_coverage[index] - if (is_covered !== line_coverage[index - 1]) { + let is_covered = line_coverage.at(index) + if (is_covered !== line_coverage.at(index - 1)) { let last_chunk = chunks.at(-1)! last_chunk.end_line = index last_chunk.total_lines = index - last_chunk.start_line + 1 @@ -207,6 +207,6 @@ export function calculate_coverage(coverage: Coverage[], parse_html?: Parser): C } } -export type { Coverage, Range } from './parse-coverage.ts' -export { parse_coverage } from './parse-coverage.ts' -export type { Parser } from './types.ts' +export type { Coverage, Range } from './parse-coverage.js' +export { parse_coverage } from './parse-coverage.js' +export type { Parser } from './types.js' diff --git a/src/lib/kitchen-sink.test.ts b/src/lib/kitchen-sink.test.ts index 30ead17..c81e3b4 100644 --- a/src/lib/kitchen-sink.test.ts +++ b/src/lib/kitchen-sink.test.ts @@ -1,5 +1,5 @@ import { test, expect } from '@playwright/test' -import { calculate_coverage } from '.' +import { calculate_coverage } from './index.js' import { DOMParser } from 'linkedom' function parse_html(html: string) { diff --git a/src/lib/parse-coverage.test.ts b/src/lib/parse-coverage.test.ts index 457953e..f146d0e 100644 --- a/src/lib/parse-coverage.test.ts +++ b/src/lib/parse-coverage.test.ts @@ -1,5 +1,5 @@ import { test, expect } from '@playwright/test' -import { parse_coverage } from './parse-coverage.ts' +import { parse_coverage } from './parse-coverage.js' test('parses valid JSON', () => { let input = ` diff --git a/src/lib/prettify.test.ts b/src/lib/prettify.test.ts index 5f2e8a7..44d4c12 100644 --- a/src/lib/prettify.test.ts +++ b/src/lib/prettify.test.ts @@ -1,5 +1,5 @@ import { test, expect } from '@playwright/test' -import { prettify } from './prettify.ts' +import { prettify } from './prettify.js' test('simple rule prettification', () => { let entries = [ diff --git a/src/lib/prettify.ts b/src/lib/prettify.ts index 65505de..3bfff3e 100644 --- a/src/lib/prettify.ts +++ b/src/lib/prettify.ts @@ -1,5 +1,5 @@ import { format } from '@projectwallace/format-css' -import type { Range, Coverage } from './parse-coverage.ts' +import type { Range, Coverage } from './parse-coverage.js' // css-tree tokens: https://github.com/csstree/csstree/blob/be5ea1257009960c04cccdb58bb327263e27e3b3/lib/tokenizer/types.js import { tokenize, tokenTypes } from 'css-tree/tokenizer' diff --git a/src/lib/remap-html.test.ts b/src/lib/remap-html.test.ts index 79896e7..593dd16 100644 --- a/src/lib/remap-html.test.ts +++ b/src/lib/remap-html.test.ts @@ -1,6 +1,6 @@ import { test, expect } from '@playwright/test' -import { remap_html } from './remap-html' import { DOMParser } from 'linkedom' +import { remap_html } from './remap-html.js' function html_parser(html: string) { return new DOMParser().parseFromString(html, 'text/html') diff --git a/src/lib/remap-html.ts b/src/lib/remap-html.ts index 1a5470e..d59d309 100644 --- a/src/lib/remap-html.ts +++ b/src/lib/remap-html.ts @@ -1,5 +1,5 @@ -import type { Parser } from './types' -import type { Range } from './parse-coverage.ts' +import type { Parser } from './types.js' +import type { Range } from './parse-coverage.js' export function remap_html(parse_html: Parser, html: string, old_ranges: Range[]) { let doc = parse_html(html) diff --git a/src/lib/test/generate-coverage.test.ts b/src/lib/test/generate-coverage.test.ts index 7a6f57f..d5aa770 100644 --- a/src/lib/test/generate-coverage.test.ts +++ b/src/lib/test/generate-coverage.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test' -import { generate_coverage } from './generate-coverage.ts' +import { generate_coverage } from './generate-coverage' test('collects coverage from html