Plan for implementing a single-file output mode for @vitest/ui's HTML reporter.
Tracking issue: vitest#6425
Add an option (e.g. reporter: [['html', { singleFile: true }]]) that produces a
single index.html with no external dependencies.
What: index.html references a JS bundle and CSS bundle (plus favicon, Google Fonts).
Where:
packages/ui/dist/client/index.html— built output- Assets:
./assets/index-*.js,./assets/index-*.css - Google Fonts: external
https://fonts.googleapis.com/...link
Plan: In writeReport(), read index.html, read each asset file, replace
<link> / <script src> tags with inline <style> / <script> blocks.
Google Fonts link can be dropped.
What: The reporter writes html.meta.json.gz and injects window.METADATA_PATH
into index.html. The client fetches it at runtime.
Where:
- Generation:
packages/ui/node/reporter.ts#L107-L114— gzip-compressed, base64-encoded - Injection:
packages/ui/node/reporter.ts#L125-L128— replaces<!-- !LOAD_METADATA! --> - Client fetch:
packages/ui/client/composables/client/static.ts#L98-L114
Plan: Instead of writing the gz file and setting METADATA_PATH to a file path,
embed the base64 gz directly as window.METADATA_DATA. Client-side: if
window.METADATA_DATA is set, skip the fetch and decode directly. Minor wiring on both
ends; the gzip/flatted pipeline is unchanged.
What: Attachments with attachment.path are copied to ./data/ and referenced as
./data/<basename>. Attachments with attachment.body are already base64 data URIs.
Where:
- URL resolution:
packages/ui/client/composables/attachments.ts#L6-L17 - Copy step:
packages/ui/node/reporter.ts#L137-L144
Plan: In self-contained mode, for each attachment.path, read the file and convert
to base64, then store as attachment.body. Skip the ./data/ copy step entirely.
No client-side changes needed — attachment.body path already renders as data URIs.
What: Coverage HTML is a full multi-file report (generated by v8/istanbul's HTML
reporter) copied to ./coverage/. The UI mounts it in an iframe at ./coverage/index.html.
Where:
- Copy step:
packages/ui/node/reporter.ts#L158-L172 - Iframe:
packages/ui/client/components/Coverage.vue#L14—src="./coverage/index.html"
Problem: Coverage is itself a multi-file HTML site (many JS/CSS/per-file HTML pages). Inlining it fully is non-trivial and coverage reporters are third-party.
Deferred as follow-up. Coverage is an MPA (many per-file HTML pages) — a general
problem tracked separately in
6425-mpa-to-single-html.md.
Once that general MPA→single-HTML tool exists, coverage inlining wires in as a consumer.
reporter.ts writeReport() in self-contained mode:
1. Read dist/client/index.html
2. Read + inline JS/CSS assets → replace <script src> / <link> tags
3. Serialize metadata → base64 gz → inject as window.METADATA_DATA inline <script>
4. Convert attachment.path entries → attachment.body (base64) in metadata
5. Skip copying ./assets/, ./data/, ./coverage/
6. Write single index.html
static.ts registerMetadata():
+ if (window.METADATA_DATA) decode directly, skip fetch