From 3afac467e892a8fa72280b476ed6d8093525debe Mon Sep 17 00:00:00 2001 From: Bart Veneman Date: Mon, 20 Oct 2025 21:50:39 +0200 Subject: [PATCH] feat: lazy-load linkedom in non-browser environments --- .oxlintrc.json | 20 +---- src/cli/cli.ts | 2 +- src/cli/program.ts | 9 +-- src/lib/filter-entries.test.ts | 32 ++------ src/lib/filter-entries.ts | 10 +-- src/lib/index.test.ts | 117 ++++++++++-------------------- src/lib/index.ts | 12 +-- src/lib/remap-html.test.ts | 21 ++---- src/lib/remap-html.ts | 19 ++++- src/lib/test/kitchen-sink.test.ts | 9 +-- src/lib/types.ts | 7 -- 11 files changed, 81 insertions(+), 177 deletions(-) delete mode 100644 src/lib/types.ts diff --git a/.oxlintrc.json b/.oxlintrc.json index 8368fe5..f92b8c3 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -52,23 +52,5 @@ "default-case": "off", "no-rest-spread-properties": "off", "require-await": "warn" - }, - "overrides": [ - { - "files": [ - "*.svelte" - ], - "rules": { - "no-unassigned-vars": "off" - } - }, - { - "files": [ - ".stylelintrc.mjs" - ], - "rules": { - "unicorn/no-null": "off" - } - } - ] + } } \ No newline at end of file diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 227379a..2f9bbdd 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -10,7 +10,7 @@ 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( + let report = await program( { min_file_coverage: params['min-line-coverage'], min_file_line_coverage: params['min-file-line-coverage'], diff --git a/src/cli/program.ts b/src/cli/program.ts index 93e0727..95f1db4 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -1,9 +1,4 @@ import { calculate_coverage, type Coverage, type CoverageResult } from '../lib/index.js' -import { DOMParser } from 'linkedom' - -function parse_html(html: string) { - return new DOMParser().parseFromString(html, 'text/html') -} export class MissingDataError extends Error { constructor() { @@ -54,7 +49,7 @@ function validate_min_file_line_coverage(actual: number, expected: number | unde } } -export function program( +export async function program( { min_file_coverage, min_file_line_coverage, @@ -67,7 +62,7 @@ export function program( if (coverage_data.length === 0) { throw new MissingDataError() } - let coverage = calculate_coverage(coverage_data, parse_html) + let coverage = await calculate_coverage(coverage_data) 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)), diff --git a/src/lib/filter-entries.test.ts b/src/lib/filter-entries.test.ts index a71850b..5bb6ed4 100644 --- a/src/lib/filter-entries.test.ts +++ b/src/lib/filter-entries.test.ts @@ -1,12 +1,7 @@ import { test, expect } from '@playwright/test' import { filter_coverage } from './filter-entries.js' -import { DOMParser } from 'linkedom' -function html_parser(html: string) { - return new DOMParser().parseFromString(html, 'text/html') -} - -test('filters out JS files', () => { +test('filters out JS files', async () => { let entries = [ { url: 'http://example.com/script.js', @@ -14,10 +9,10 @@ test('filters out JS files', () => { ranges: [{ start: 0, end: 25 }], }, ] - expect(filter_coverage(entries, html_parser)).toEqual([]) + expect(await filter_coverage(entries)).toEqual([]) }) -test('keeps files with CSS extension', () => { +test('keeps files with CSS extension', async () => { let entries = [ { url: 'http://example.com/styles.css', @@ -25,10 +20,10 @@ test('keeps files with CSS extension', () => { ranges: [{ start: 0, end: 13 }], }, ] - expect(filter_coverage(entries, html_parser)).toEqual(entries) + expect(await filter_coverage(entries)).toEqual(entries) }) -test('keeps extension-less URL with HTML text', () => { +test('keeps extension-less URL with HTML text', async () => { let entries = [ { url: 'http://example.com', @@ -43,10 +38,10 @@ test('keeps extension-less URL with HTML text', () => { ranges: [{ start: 0, end: 13 }], // ranges are remapped }, ] - expect(filter_coverage(entries, html_parser)).toEqual(expected) + expect(await filter_coverage(entries)).toEqual(expected) }) -test('keeps extension-less URL with CSS text (running coverage in vite dev mode)', () => { +test('keeps extension-less URL with CSS text (running coverage in vite dev mode)', async () => { let entries = [ { url: 'http://example.com', @@ -54,16 +49,5 @@ test('keeps extension-less URL with CSS text (running coverage in vite dev mode) ranges: [{ start: 0, end: 13 }], }, ] - expect(filter_coverage(entries, html_parser)).toEqual(entries) -}) - -test('skips extension-less URL with HTML text when no parser is provided', () => { - let entries = [ - { - url: 'http://example.com', - text: ``, - ranges: [{ start: 13, end: 26 }], - }, - ] - expect(filter_coverage(entries)).toEqual([]) + expect(await filter_coverage(entries)).toEqual(entries) }) diff --git a/src/lib/filter-entries.ts b/src/lib/filter-entries.ts index a355b69..eb80726 100644 --- a/src/lib/filter-entries.ts +++ b/src/lib/filter-entries.ts @@ -1,13 +1,12 @@ 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) } -export function filter_coverage(coverage: Coverage[], parse_html?: Parser): Coverage[] { +export async function filter_coverage(coverage: Coverage[]): Promise { let result = [] for (let entry of coverage) { @@ -21,12 +20,7 @@ export function filter_coverage(coverage: Coverage[], parse_html?: Parser): Cove } if (is_html(entry.text)) { - if (!parse_html) { - // No parser provided, cannot extract CSS from HTML, silently skip this entry - continue - } - - let { css, ranges } = remap_html(parse_html, entry.text, entry.ranges) + let { css, ranges } = await remap_html(entry.text, entry.ranges) result.push({ url: entry.url, text: css, diff --git a/src/lib/index.test.ts b/src/lib/index.test.ts index da62b6a..83e634d 100644 --- a/src/lib/index.test.ts +++ b/src/lib/index.test.ts @@ -3,11 +3,6 @@ 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' - -function html_parser(html: string) { - return new DOMParser().parseFromString(html, 'text/html') -} test.describe('from ') - let result = remap_html(html_parser, html, [{ start: 1, end: 2 }]) + let result = await remap_html(html, [{ start: 1, end: 2 }]) expect(result).toEqual({ css: '', ranges: [], }) }) -test('skips white-space-only style block', () => { +test('skips white-space-only style block', async () => { let html = create_html(``) - let result = remap_html(html_parser, html, [{ start: 1, end: 2 }]) + let result = await remap_html(html, [{ start: 1, end: 2 }]) expect(result).toEqual({ css: '', ranges: [], }) }) -test('remaps a single style block', () => { +test('remaps a single style block', async () => { let css = `h1 { color: red; }` let html = create_html(``, `

Hello world

`) let range = { start: html.indexOf(css), end: html.indexOf(css) + css.length } - let result = remap_html(html_parser, html, [range]) + let result = await remap_html(html, [range]) expect(result).toEqual({ css, ranges: [{ start: 0, end: css.length }], }) }) -test('remaps multiple style blocks', () => { +test('remaps multiple style blocks', async () => { let css_head = `h1 { color: red; }` let css_body = `h2 { font-size: 24px; }` let html = create_html(``, ``) let range_head = { start: html.indexOf(css_head), end: html.indexOf(css_head) + css_head.length } let range_body = { start: html.indexOf(css_body), end: html.indexOf(css_body) + css_body.length } - let result = remap_html(html_parser, html, [range_head, range_body]) + let result = await remap_html(html, [range_head, range_body]) expect(result).toEqual({ css: css_head + css_body, ranges: [ diff --git a/src/lib/remap-html.ts b/src/lib/remap-html.ts index d59d309..10bbb39 100644 --- a/src/lib/remap-html.ts +++ b/src/lib/remap-html.ts @@ -1,12 +1,23 @@ -import type { Parser } from './types.js' import type { Range } from './parse-coverage.js' +import type { DOMParser as LinkedomParser } from 'linkedom' -export function remap_html(parse_html: Parser, html: string, old_ranges: Range[]) { - let doc = parse_html(html) +async function get_dom_parser(): Promise { + if (typeof window !== 'undefined' && 'DOMParser' in window) { + /* v8 ignore */ + return new window.DOMParser() + } + + let { DOMParser } = await import('linkedom') + return new DOMParser() as LinkedomParser +} + +export async function remap_html(html: string, old_ranges: Range[]) { + let dom_parser = await get_dom_parser() + let doc = dom_parser.parseFromString(html, 'text/html') let combined_css = '' let new_ranges = [] let current_offset = 0 - let style_elements = doc.querySelectorAll('style') + let style_elements = doc.querySelectorAll('style') as NodeListOf for (let style_element of Array.from(style_elements)) { let style_content = style_element.textContent diff --git a/src/lib/test/kitchen-sink.test.ts b/src/lib/test/kitchen-sink.test.ts index 3f7a91c..fde52da 100644 --- a/src/lib/test/kitchen-sink.test.ts +++ b/src/lib/test/kitchen-sink.test.ts @@ -1,12 +1,7 @@ import { test, expect } from '@playwright/test' import { calculate_coverage } from '../index.js' -import { DOMParser } from 'linkedom' -function parse_html(html: string) { - return new DOMParser().parseFromString(html, 'text/html') -} - -test('project wallace Container component', () => { +test('project wallace Container component', async () => { const coverage = [ { url: 'http://localhost:4173/_app/immutable/assets/Container.n-2BXq6O.css', @@ -20,7 +15,7 @@ test('project wallace Container component', () => { ], }, ] - let result = calculate_coverage(coverage, parse_html) + let result = await calculate_coverage(coverage) let sheet = result.coverage_per_stylesheet.at(0)! expect.soft(sheet.total_lines).toBe(44) diff --git a/src/lib/types.ts b/src/lib/types.ts deleted file mode 100644 index 433468d..0000000 --- a/src/lib/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -type NodeList = Iterable<{ textContent: string }> | NodeListOf - -export interface Parser { - (html: string): { - querySelectorAll: (selector: string) => NodeList - } -}