Skip to content

Commit 1d70494

Browse files
authored
fix: avoid mutations so lib can be used in a Svelte $derived() (#43)
* fix: avoid mutations so lib can be used in a Svelte `$derived()` * knip
1 parent 7712027 commit 1d70494

File tree

8 files changed

+170
-236
lines changed

8 files changed

+170
-236
lines changed

src/lib/chunkify.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export type ChunkedCoverage = Omit<Coverage, 'ranges'> & {
1010
chunks: Chunk[]
1111
}
1212

13+
const WHITESPACE_ONLY_REGEX = /^\s+$/
14+
1315
function merge(stylesheet: ChunkedCoverage): ChunkedCoverage {
1416
let new_chunks: Chunk[] = []
1517
let previous_chunk: Chunk | undefined
@@ -18,7 +20,7 @@ function merge(stylesheet: ChunkedCoverage): ChunkedCoverage {
1820
let chunk = stylesheet.chunks.at(i)!
1921

2022
// If the current chunk is only whitespace or empty, ignore it
21-
if (/^\s+$/.test(stylesheet.text.slice(chunk.start_offset, chunk.end_offset))) {
23+
if (WHITESPACE_ONLY_REGEX.test(stylesheet.text.slice(chunk.start_offset, chunk.end_offset))) {
2224
continue
2325
}
2426

@@ -32,7 +34,10 @@ function merge(stylesheet: ChunkedCoverage): ChunkedCoverage {
3234
continue
3335
}
3436
// If the current chunk is only whitespace or empty, add it to the previous
35-
else if (/^\s+$/.test(stylesheet.text.slice(chunk.start_offset, chunk.end_offset)) || chunk.end_offset === chunk.start_offset) {
37+
else if (
38+
WHITESPACE_ONLY_REGEX.test(stylesheet.text.slice(chunk.start_offset, chunk.end_offset)) ||
39+
chunk.end_offset === chunk.start_offset
40+
) {
3641
latest_chunk.end_offset = chunk.end_offset
3742
// do not update previous_chunk
3843
continue

src/lib/decuplicate.ts

Lines changed: 38 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,60 @@
11
import type { Coverage, Range } from './parse-coverage.js'
22

3-
// Combine multiple adjecent ranges into a single one
4-
function concatenate(ranges: Set<Range> | Range[]): Range[] {
5-
let result: Range[] = []
3+
// 1. Merge and concatenate ranges
4+
function merge_ranges(ranges: Range[]): Range[] {
5+
if (ranges.length === 0) return []
66

7-
for (let range of ranges) {
8-
// Update the last range if this range starts at last-range-end + 1
9-
if (result.length > 0 && (result.at(-1)!.end === range.start - 1 || result.at(-1)!.end === range.start)) {
10-
result.at(-1)!.end = range.end
7+
// sort by start
8+
ranges.sort((a, b) => a.start - b.start)
9+
10+
let merged: Range[] = [ranges[0]!]
11+
12+
for (let r of ranges.slice(1)) {
13+
let last = merged.at(-1)
14+
15+
// merge overlapping or adjacent
16+
if (last && r.start <= last.end + 1) {
17+
if (r.end > last.end) {
18+
last.end = r.end
19+
}
1120
} else {
12-
result.push(range)
21+
merged.push({ start: r.start, end: r.end })
1322
}
1423
}
1524

16-
return result
25+
return merged
1726
}
1827

19-
function dedupe_list(ranges: Range[]): Set<Range> {
20-
let new_ranges: Set<Range> = new Set()
28+
// 2. Merge ranges for a single stylesheet entry into an existing grouped sheet
29+
function merge_entry_ranges(sheet: { url: string; ranges: Range[] } | undefined, entry: Coverage): { url: string; ranges: Range[] } {
30+
if (!sheet) {
31+
return { url: entry.url, ranges: [...entry.ranges] }
32+
}
2133

22-
outer: for (let range of ranges) {
23-
for (let processed_range of new_ranges) {
24-
// Case: an existing range fits within this range -> replace it
25-
// { start: 0, end: 100 },
26-
// { start: 0, end: 200 }
27-
if (range.start <= processed_range.start && range.end >= processed_range.end) {
28-
new_ranges.delete(processed_range)
29-
new_ranges.add(range)
30-
continue outer
31-
}
34+
let seen = new Set(sheet.ranges.map((r) => `${r.start}:${r.end}`))
3235

33-
// Case: this range fits within an existing range -> skip it
34-
// { start: 324, end: 485 }, --> exists
35-
// { start: 364, end: 485 }, --> skip
36-
// { start: 404, end: 485 }, --> skip
37-
if (range.start >= processed_range.start && range.end <= processed_range.end) {
38-
// console.log('skip', range)
39-
continue outer
40-
}
41-
// Case: ranges partially overlap
42-
// { start: 324, end: 444 },
43-
// { start: 364, end: 485 },
44-
if (range.start < processed_range.end && range.start > processed_range.start && range.end > processed_range.end) {
45-
new_ranges.delete(processed_range)
46-
new_ranges.add({
47-
start: processed_range.start,
48-
end: range.end,
49-
})
50-
continue outer
51-
}
36+
for (let range of entry.ranges) {
37+
let id = `${range.start}:${range.end}`
38+
if (!seen.has(id)) {
39+
seen.add(id)
40+
sheet.ranges.push({ ...range })
5241
}
53-
new_ranges.add(range)
5442
}
5543

56-
return new_ranges
44+
return sheet
5745
}
5846

59-
/**
60-
* @description
61-
* prerequisites
62-
* - we check each stylesheet content only once (to avoid counting the same content multiple times)
63-
* - if a duplicate stylesheet enters the room, we add it's ranges to the existing stylesheet's ranges
64-
* - only bytes of deduplicated stylesheets are counted
65-
*/
47+
// 3. Main function orchestrating the grouping and range merging
6648
export function deduplicate_entries(entries: Coverage[]): Coverage[] {
67-
let checked_stylesheets = new Map<string, { url: string; ranges: Range[] }>()
68-
69-
for (let entry of entries) {
70-
let text = entry.text
71-
if (checked_stylesheets.has(text)) {
72-
let sheet = checked_stylesheets.get(text)!
73-
let ranges = sheet.ranges
74-
// Check if the ranges are already in the checked_stylesheets map
75-
// If not, add them
76-
for (let range of entry.ranges) {
77-
let found = false
78-
79-
for (let checked_range of ranges) {
80-
// find exact range
81-
if (checked_range.start === range.start && checked_range.end === range.end) {
82-
found = true
83-
break
84-
}
85-
}
86-
87-
if (!found) {
88-
ranges.push(range)
89-
}
90-
}
91-
} else {
92-
checked_stylesheets.set(text, {
93-
url: entry.url,
94-
ranges: entry.ranges,
95-
})
96-
}
97-
}
49+
let grouped = entries.reduce<Record<string, { url: string; ranges: Range[] }>>((acc, entry) => {
50+
let key = entry.text
51+
acc[key] = merge_entry_ranges(acc[key], entry)
52+
return acc
53+
}, Object.create(null))
9854

99-
return Array.from(checked_stylesheets, ([text, { url, ranges }]) => ({
55+
return Object.entries(grouped).map(([text, { url, ranges }]) => ({
10056
text,
10157
url,
102-
ranges: concatenate(dedupe_list(ranges.sort((a, b) => a.start - b.start))).sort((a, b) => a.start - b.start),
58+
ranges: merge_ranges(ranges),
10359
}))
10460
}

src/lib/extend-ranges.test.ts

Lines changed: 44 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ test.describe('leaves ranges intact when nothing to change', () => {
2424
// Expect the incomplete coverage reported by the browser
2525
expect(coverage.at(0)!.ranges).toEqual([{ start: 0, end: 17 }])
2626

27-
let result = extend_ranges(coverage)
28-
expect(result.at(0)!.ranges).toEqual([{ start: 0, end: 17 }])
27+
let result = extend_ranges(coverage[0])
28+
expect(result.ranges).toEqual([{ start: 0, end: 17 }])
2929
})
3030
})
3131

@@ -37,8 +37,8 @@ test.describe('@rules', () => {
3737
// Expect the incomplete coverage reported by the browser
3838
expect(coverage.at(0)!.ranges).toEqual([{ start: 7, end: 42 }]) // (min-width:100px){body{color:green}
3939

40-
let result = extend_ranges(coverage)
41-
expect(result.at(0)!.ranges).toEqual([{ start: 0, end: 43 }]) // @media (min-width:100px){body{color:green}}
40+
let result = extend_ranges(coverage[0])
41+
expect(result.ranges).toEqual([{ start: 0, end: 43 }]) // @media (min-width:100px){body{color:green}}
4242
})
4343

4444
test.describe('adjecent to uncovered code', () => {
@@ -49,8 +49,8 @@ test.describe('@rules', () => {
4949
// Expect the incomplete coverage reported by the browser
5050
expect(coverage.at(0)!.ranges).toEqual([{ start: 10, end: 45 }]) // (min-width:100px){body{color:green}
5151

52-
let result = extend_ranges(coverage)
53-
expect(result.at(0)!.ranges).toEqual([{ start: 3, end: 46 }]) // @media (min-width:100px){body{color:green}}
52+
let result = extend_ranges(coverage[0])
53+
expect(result.ranges).toEqual([{ start: 3, end: 46 }]) // @media (min-width:100px){body{color:green}}
5454
})
5555

5656
test('@media at start', async () => {
@@ -60,8 +60,8 @@ test.describe('@rules', () => {
6060
// Expect the incomplete coverage reported by the browser
6161
expect(coverage.at(0)!.ranges).toEqual([{ start: 7, end: 42 }]) // (min-width:100px){body{color:green}
6262

63-
let result = extend_ranges(coverage)
64-
expect(result.at(0)!.ranges).toEqual([{ start: 0, end: 43 }]) // @media (min-width:100px){body{color:green}}
63+
let result = extend_ranges(coverage[0])
64+
expect(result.ranges).toEqual([{ start: 0, end: 43 }]) // @media (min-width:100px){body{color:green}}
6565
})
6666
})
6767

@@ -71,57 +71,49 @@ test.describe('@rules', () => {
7171
let coverage = (await generate_coverage(html, { link_css: css })) as Coverage[]
7272

7373
// Expect the incomplete coverage reported by the browser
74-
expect(coverage).toEqual([
75-
{
76-
url: 'http://localhost/style.css',
77-
text: css,
78-
ranges: [
79-
{ start: 0, end: 12 }, // p{color:red}
80-
{ start: 19, end: 54 }, // (min-width:100px){body{color:green}
81-
],
82-
},
83-
])
84-
85-
let result = extend_ranges(coverage)
86-
expect(result).toEqual([
87-
{
88-
url: 'http://localhost/style.css',
89-
text: css,
90-
ranges: [
91-
{ start: 0, end: 12 }, // p{color:red}
92-
{ start: 12, end: 55 }, // @media (min-width:100px){body{color:green}}
93-
],
94-
},
95-
])
74+
expect(coverage[0]).toEqual({
75+
url: 'http://localhost/style.css',
76+
text: css,
77+
ranges: [
78+
{ start: 0, end: 12 }, // p{color:red}
79+
{ start: 19, end: 54 }, // (min-width:100px){body{color:green}
80+
],
81+
})
82+
83+
let result = extend_ranges(coverage[0])
84+
expect(result).toEqual({
85+
url: 'http://localhost/style.css',
86+
text: css,
87+
ranges: [
88+
{ start: 0, end: 12 }, // p{color:red}
89+
{ start: 12, end: 55 }, // @media (min-width:100px){body{color:green}}
90+
],
91+
})
9692
})
9793

9894
test('@media at start', async () => {
9995
let css = `@media (min-width:100px){body{color:green}}p{color:red}`
10096
let coverage = (await generate_coverage(html, { link_css: css })) as Coverage[]
10197

10298
// Expect the incomplete coverage reported by the browser
103-
expect(coverage).toEqual([
104-
{
105-
url: 'http://localhost/style.css',
106-
text: css,
107-
ranges: [
108-
{ start: 7, end: 42 }, // (min-width:100px){body{color:green}
109-
{ start: 43, end: 55 }, // p{color:red}
110-
],
111-
},
112-
])
113-
114-
let result = extend_ranges(coverage)
115-
expect(result).toEqual([
116-
{
117-
url: 'http://localhost/style.css',
118-
text: css,
119-
ranges: [
120-
{ start: 0, end: 43 }, // @media (min-width:100px){body{color:green}}
121-
{ start: 43, end: 55 }, // p{color:red}
122-
],
123-
},
124-
])
99+
expect(coverage[0]).toEqual({
100+
url: 'http://localhost/style.css',
101+
text: css,
102+
ranges: [
103+
{ start: 7, end: 42 }, // (min-width:100px){body{color:green}
104+
{ start: 43, end: 55 }, // p{color:red}
105+
],
106+
})
107+
108+
let result = extend_ranges(coverage[0])
109+
expect(result).toEqual({
110+
url: 'http://localhost/style.css',
111+
text: css,
112+
ranges: [
113+
{ start: 0, end: 43 }, // @media (min-width:100px){body{color:green}}
114+
{ start: 43, end: 55 }, // p{color:red}
115+
],
116+
})
125117
})
126118
})
127119
})

0 commit comments

Comments
 (0)