Skip to content

Commit 3afac46

Browse files
committed
feat: lazy-load linkedom in non-browser environments
1 parent 2d81ffb commit 3afac46

File tree

11 files changed

+81
-177
lines changed

11 files changed

+81
-177
lines changed

.oxlintrc.json

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -52,23 +52,5 @@
5252
"default-case": "off",
5353
"no-rest-spread-properties": "off",
5454
"require-await": "warn"
55-
},
56-
"overrides": [
57-
{
58-
"files": [
59-
"*.svelte"
60-
],
61-
"rules": {
62-
"no-unassigned-vars": "off"
63-
}
64-
},
65-
{
66-
"files": [
67-
".stylelintrc.mjs"
68-
],
69-
"rules": {
70-
"unicorn/no-null": "off"
71-
}
72-
}
73-
]
55+
}
7456
}

src/cli/cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ async function cli(cli_args: string[]) {
1010
const args = parse_arguments(cli_args)
1111
let params = validate_arguments(args)
1212
let coverage_data = await read(params['coverage-dir'])
13-
let report = program(
13+
let report = await program(
1414
{
1515
min_file_coverage: params['min-line-coverage'],
1616
min_file_line_coverage: params['min-file-line-coverage'],

src/cli/program.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
11
import { calculate_coverage, type Coverage, type CoverageResult } from '../lib/index.js'
2-
import { DOMParser } from 'linkedom'
3-
4-
function parse_html(html: string) {
5-
return new DOMParser().parseFromString(html, 'text/html')
6-
}
72

83
export class MissingDataError extends Error {
94
constructor() {
@@ -54,7 +49,7 @@ function validate_min_file_line_coverage(actual: number, expected: number | unde
5449
}
5550
}
5651

57-
export function program(
52+
export async function program(
5853
{
5954
min_file_coverage,
6055
min_file_line_coverage,
@@ -67,7 +62,7 @@ export function program(
6762
if (coverage_data.length === 0) {
6863
throw new MissingDataError()
6964
}
70-
let coverage = calculate_coverage(coverage_data, parse_html)
65+
let coverage = await calculate_coverage(coverage_data)
7166
let min_line_coverage_result = validate_min_line_coverage(coverage.line_coverage_ratio, min_file_coverage)
7267
let min_file_line_coverage_result = validate_min_file_line_coverage(
7368
Math.min(...coverage.coverage_per_stylesheet.map((sheet) => sheet.line_coverage_ratio)),

src/lib/filter-entries.test.ts

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,29 @@
11
import { test, expect } from '@playwright/test'
22
import { filter_coverage } from './filter-entries.js'
3-
import { DOMParser } from 'linkedom'
43

5-
function html_parser(html: string) {
6-
return new DOMParser().parseFromString(html, 'text/html')
7-
}
8-
9-
test('filters out JS files', () => {
4+
test('filters out JS files', async () => {
105
let entries = [
116
{
127
url: 'http://example.com/script.js',
138
text: 'console.log("Hello world")',
149
ranges: [{ start: 0, end: 25 }],
1510
},
1611
]
17-
expect(filter_coverage(entries, html_parser)).toEqual([])
12+
expect(await filter_coverage(entries)).toEqual([])
1813
})
1914

20-
test('keeps files with CSS extension', () => {
15+
test('keeps files with CSS extension', async () => {
2116
let entries = [
2217
{
2318
url: 'http://example.com/styles.css',
2419
text: 'a{color:red}',
2520
ranges: [{ start: 0, end: 13 }],
2621
},
2722
]
28-
expect(filter_coverage(entries, html_parser)).toEqual(entries)
23+
expect(await filter_coverage(entries)).toEqual(entries)
2924
})
3025

31-
test('keeps extension-less URL with HTML text', () => {
26+
test('keeps extension-less URL with HTML text', async () => {
3227
let entries = [
3328
{
3429
url: 'http://example.com',
@@ -43,27 +38,16 @@ test('keeps extension-less URL with HTML text', () => {
4338
ranges: [{ start: 0, end: 13 }], // ranges are remapped
4439
},
4540
]
46-
expect(filter_coverage(entries, html_parser)).toEqual(expected)
41+
expect(await filter_coverage(entries)).toEqual(expected)
4742
})
4843

49-
test('keeps extension-less URL with CSS text (running coverage in vite dev mode)', () => {
44+
test('keeps extension-less URL with CSS text (running coverage in vite dev mode)', async () => {
5045
let entries = [
5146
{
5247
url: 'http://example.com',
5348
text: 'a{color:red;}',
5449
ranges: [{ start: 0, end: 13 }],
5550
},
5651
]
57-
expect(filter_coverage(entries, html_parser)).toEqual(entries)
58-
})
59-
60-
test('skips extension-less URL with HTML text when no parser is provided', () => {
61-
let entries = [
62-
{
63-
url: 'http://example.com',
64-
text: `<html><style>a{color:red;}</style></html>`,
65-
ranges: [{ start: 13, end: 26 }],
66-
},
67-
]
68-
expect(filter_coverage(entries)).toEqual([])
52+
expect(await filter_coverage(entries)).toEqual(entries)
6953
})

src/lib/filter-entries.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import type { Coverage } from './parse-coverage.js'
22
import { ext } from './ext.js'
3-
import type { Parser } from './types.js'
43
import { remap_html } from './remap-html.js'
54

65
function is_html(text: string): boolean {
76
return /<\/?(html|body|head|div|span|script|style)/i.test(text)
87
}
98

10-
export function filter_coverage(coverage: Coverage[], parse_html?: Parser): Coverage[] {
9+
export async function filter_coverage(coverage: Coverage[]): Promise<Coverage[]> {
1110
let result = []
1211

1312
for (let entry of coverage) {
@@ -21,12 +20,7 @@ export function filter_coverage(coverage: Coverage[], parse_html?: Parser): Cove
2120
}
2221

2322
if (is_html(entry.text)) {
24-
if (!parse_html) {
25-
// No parser provided, cannot extract CSS from HTML, silently skip this entry
26-
continue
27-
}
28-
29-
let { css, ranges } = remap_html(parse_html, entry.text, entry.ranges)
23+
let { css, ranges } = await remap_html(entry.text, entry.ranges)
3024
result.push({
3125
url: entry.url,
3226
text: css,

src/lib/index.test.ts

Lines changed: 39 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,6 @@ import { generate_coverage } from './test/generate-coverage.js'
33
import { calculate_coverage } from './index.js'
44
import type { Coverage } from './parse-coverage.js'
55
import { format } from '@projectwallace/format-css'
6-
import { DOMParser } from 'linkedom'
7-
8-
function html_parser(html: string) {
9-
return new DOMParser().parseFromString(html, 'text/html')
10-
}
116

127
test.describe('from <style> tag', () => {
138
let coverage: Coverage[]
@@ -32,8 +27,8 @@ test.describe('from <style> tag', () => {
3227
coverage = (await generate_coverage(html)) as Coverage[]
3328
})
3429

35-
test('counts totals', () => {
36-
let result = calculate_coverage(coverage, html_parser)
30+
test('counts totals', async () => {
31+
let result = await calculate_coverage(coverage)
3732
expect.soft(result.total_files_found).toBe(1)
3833
expect.soft(result.total_bytes).toBe(80)
3934
expect.soft(result.used_bytes).toBe(42)
@@ -45,8 +40,8 @@ test.describe('from <style> tag', () => {
4540
expect.soft(result.total_stylesheets).toBe(1)
4641
})
4742

48-
test('calculates stats per stylesheet', () => {
49-
let result = calculate_coverage(coverage, html_parser)
43+
test('calculates stats per stylesheet', async () => {
44+
let result = await calculate_coverage(coverage)
5045
let sheet = result.coverage_per_stylesheet.at(0)!
5146
expect.soft(sheet.url).toBe('http://localhost/test.html')
5247
expect.soft(sheet.ranges).toEqual([
@@ -89,8 +84,8 @@ test.describe('from <link rel="stylesheet">', () => {
8984
coverage = (await generate_coverage(html, { link_css: css })) as Coverage[]
9085
})
9186

92-
test('counts totals', () => {
93-
let result = calculate_coverage(coverage, html_parser)
87+
test('counts totals', async () => {
88+
let result = await calculate_coverage(coverage)
9489
expect.soft(result.total_files_found).toBe(1)
9590
expect.soft(result.total_bytes).toBe(174)
9691
expect.soft(result.used_bytes).toBe(91)
@@ -102,8 +97,8 @@ test.describe('from <link rel="stylesheet">', () => {
10297
expect.soft(result.total_stylesheets).toBe(1)
10398
})
10499

105-
test('calculates stats per stylesheet', () => {
106-
let result = calculate_coverage(coverage, html_parser)
100+
test('calculates stats per stylesheet', async () => {
101+
let result = await calculate_coverage(coverage)
107102
let sheet = result.coverage_per_stylesheet.at(0)!
108103
expect.soft(sheet.url).toBe('http://localhost/style.css')
109104
expect.soft(sheet.ranges).toEqual([
@@ -148,17 +143,17 @@ test.describe('from coverage data downloaded directly from the browser as JSON',
148143
},
149144
]
150145

151-
test('counts totals', () => {
152-
let result = calculate_coverage(coverage, html_parser)
146+
test('counts totals', async () => {
147+
let result = await calculate_coverage(coverage)
153148
expect.soft(result.covered_lines).toBe(9)
154149
expect.soft(result.uncovered_lines).toBe(5)
155150
expect.soft(result.total_lines).toBe(14)
156151
expect.soft(result.line_coverage_ratio).toBe(9 / 14)
157152
expect.soft(result.total_stylesheets).toBe(1)
158153
})
159154

160-
test('extracts and formats css', () => {
161-
let result = calculate_coverage(coverage, html_parser)
155+
test('extracts and formats css', async () => {
156+
let result = await calculate_coverage(coverage)
162157
expect(result.coverage_per_stylesheet.at(0)?.text).toEqual(
163158
format(`h1 {
164159
color: blue;
@@ -178,8 +173,8 @@ test.describe('from coverage data downloaded directly from the browser as JSON',
178173
)
179174
})
180175

181-
test('calculates line coverage', () => {
182-
let result = calculate_coverage(coverage, html_parser)
176+
test('calculates line coverage', async () => {
177+
let result = await calculate_coverage(coverage)
183178
expect(result.coverage_per_stylesheet.at(0)?.line_coverage).toEqual(
184179
new Uint8Array([
185180
// h1 {}
@@ -198,8 +193,8 @@ test.describe('from coverage data downloaded directly from the browser as JSON',
198193
)
199194
})
200195

201-
test('calculates chunks', () => {
202-
let result = calculate_coverage(coverage, html_parser)
196+
test('calculates chunks', async () => {
197+
let result = await calculate_coverage(coverage)
203198
expect(result.coverage_per_stylesheet.at(0)?.chunks).toEqual([
204199
{ start_line: 1, is_covered: true, end_line: 4, total_lines: 4 },
205200
{ start_line: 5, is_covered: false, end_line: 8, total_lines: 4 },
@@ -209,22 +204,19 @@ test.describe('from coverage data downloaded directly from the browser as JSON',
209204
])
210205
})
211206

212-
test('calculates chunks for fully covered file', () => {
213-
let result = calculate_coverage(
214-
[
215-
{
216-
url: 'https://example.com',
217-
ranges: [
218-
{
219-
start: 0,
220-
end: 19,
221-
},
222-
],
223-
text: 'h1 { color: blue; }',
224-
},
225-
],
226-
html_parser,
227-
)
207+
test('calculates chunks for fully covered file', async () => {
208+
let result = await calculate_coverage([
209+
{
210+
url: 'https://example.com',
211+
ranges: [
212+
{
213+
start: 0,
214+
end: 19,
215+
},
216+
],
217+
text: 'h1 { color: blue; }',
218+
},
219+
])
228220
expect(result.coverage_per_stylesheet.at(0)?.text).toEqual('h1 {\n\tcolor: blue;\n}')
229221
expect(result.coverage_per_stylesheet.at(0)?.chunks).toEqual([
230222
{
@@ -236,17 +228,14 @@ test.describe('from coverage data downloaded directly from the browser as JSON',
236228
])
237229
})
238230

239-
test('calculates chunks for fully uncovered file', () => {
240-
let result = calculate_coverage(
241-
[
242-
{
243-
url: 'https://example.com',
244-
ranges: [],
245-
text: 'h1 { color: blue; }',
246-
},
247-
],
248-
html_parser,
249-
)
231+
test('calculates chunks for fully uncovered file', async () => {
232+
let result = await calculate_coverage([
233+
{
234+
url: 'https://example.com',
235+
ranges: [],
236+
text: 'h1 { color: blue; }',
237+
},
238+
])
250239
expect(result.coverage_per_stylesheet.at(0)?.chunks).toEqual([
251240
{
252241
start_line: 1,
@@ -258,8 +247,8 @@ test.describe('from coverage data downloaded directly from the browser as JSON',
258247
})
259248
})
260249

261-
test('handles empty input', () => {
262-
let result = calculate_coverage([], html_parser)
250+
test('handles empty input', async () => {
251+
let result = await calculate_coverage([])
263252
expect(result.total_files_found).toBe(0)
264253
expect(result.total_bytes).toBe(0)
265254
expect(result.used_bytes).toBe(0)
@@ -271,31 +260,3 @@ test('handles empty input', () => {
271260
expect(result.total_stylesheets).toBe(0)
272261
expect(result.coverage_per_stylesheet).toEqual([])
273262
})
274-
275-
test.describe('garbage input', () => {
276-
test('garbage Array', () => {
277-
expect(() =>
278-
calculate_coverage(
279-
[
280-
{
281-
test: 1,
282-
garbage: true,
283-
},
284-
] as unknown as Coverage[],
285-
html_parser,
286-
),
287-
).toThrow('No valid coverage data found')
288-
})
289-
290-
test('garbage Object', () => {
291-
expect(() =>
292-
calculate_coverage(
293-
{
294-
test: 1,
295-
garbage: true,
296-
} as unknown as Coverage[],
297-
html_parser,
298-
),
299-
).toThrow('No valid coverage data found')
300-
})
301-
})

src/lib/index.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { is_valid_coverage, type Coverage, type Range } from './parse-coverage.js'
1+
import { type Coverage, type Range } from './parse-coverage.js'
22
import { prettify } from './prettify.js'
33
import { deduplicate_entries } from './decuplicate.js'
44
import { filter_coverage } from './filter-entries.js'
5-
import type { Parser } from './types.js'
65

76
export type CoverageData = {
87
unused_bytes: number
@@ -50,14 +49,10 @@ function ratio(fraction: number, total: number) {
5049
* 4. Calculate used/unused CSS bytes (fastest path, no inspection of the actual CSS needed)
5150
* 5. Calculate line-coverage, byte-coverage per stylesheet
5251
*/
53-
export function calculate_coverage(coverage: Coverage[], parse_html?: Parser): CoverageResult {
52+
export async function calculate_coverage(coverage: Coverage[]): Promise<CoverageResult> {
5453
let total_files_found = coverage.length
5554

56-
if (!is_valid_coverage(coverage)) {
57-
throw new TypeError('No valid coverage data found')
58-
}
59-
60-
let filtered_coverage: Coverage[] = filter_coverage(coverage, parse_html)
55+
let filtered_coverage: Coverage[] = await filter_coverage(coverage)
6156
let prettified_coverage: Coverage[] = prettify(filtered_coverage)
6257
let deduplicated: Coverage[] = deduplicate_entries(prettified_coverage)
6358

@@ -209,4 +204,3 @@ export function calculate_coverage(coverage: Coverage[], parse_html?: Parser): C
209204

210205
export type { Coverage, Range } from './parse-coverage.js'
211206
export { parse_coverage } from './parse-coverage.js'
212-
export type { Parser } from './types.js'

0 commit comments

Comments
 (0)