wxt-dev / wxt-dev/wxt

Auto-imports emit a line-only sourcemap, silently collapsing code coverage (regression since v0.19.27, affects build and test)

Open
#2,604 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

pending-triage
Dominant language
TypeScript
Stars
10.5k
Forks
564
PR merge metrics
No merged PRs in 30d

Description

Describe the bug

In a project using WxtVitest(), @vitest/coverage-v8 (and -istanbul) report near-total coverage collapse for every module that references an auto-imported global. The whole module body maps to a single statement, so it is neither counted as covered nor as uncovered — it vanishes from the denominator.

In my project this made a coverage.thresholds gate of 100% pass while the real figure was 90%: coverage measured 871 of 3,390 statements, hiding ~300 genuinely uncovered lines. It fails open and silently, which is the dangerous part — a threshold gate reports green precisely because the code is invisible.

A 1,139-line component came out as:

SF:src/components/ConfigForm.tsx
DA:65,3
DA:89,3
LF:2
LH:2

Line 65 is a top-level const; line 89 is the component's declaration. Istanbul's statement map holds one statement spanning lines 89 → 1137, with column: null throughout.

Cause

WxtVitest registers the upstream unimport unplugin directly:

// wxt/dist/testing/wxt-vitest-plugin.mjs
plugins.push(UnimportPlugin.vite(wxt.config.imports));

and unimport/unplugin generates its sourcemap with no options:

// unimport@6.4.0/dist/unplugin.mjs
return {
  code: s.toString(),
  map: s.generateMap(),   // MagicString defaults to hires: false → no column mappings
};

The plugin is enforce: 'post', so it runs after the TS/JSX transform and its line-only map is the last in the chain, discarding the column fidelity everything upstream produced. Modules where s.hasChanged() is false are untouched and measure correctly — which is exactly the observed split between files that use browser/storage and files that don't.

It is also not only the missing columns: source is never passed either, so the emitted map carries sources: [""]. Reproduced against unimport alone, with no bundler involved:

current (s.generateMap()):
  sources          : [""]
  segments per line: [0,1,1,1,1,1,1,0]

with hires: 'boundary' + source:
  sources          : ["/src/a.ts"]
  segments per line: [0,10,12,18,18,8,1,0]

This is a regression

#381 did add the fix — but to WXT's own wxt:unimport Vite plugin:

map: injected.s.generateMap({ hires: 'boundary', source: id }),

#1412 then deleted that plugin and pointed both the build path and WxtVitest() at UnimportPlugin.vite():

     addViteConfig(wxt, () => ({
-      plugins: [vitePlugin(unimport)],
+      plugins: [UnimportPlugin.vite(options)],
     }));
-    plugins.push(unimportPlugin(unimport));
+    plugins.push(UnimportPlugin.vite(wxt.config.imports));

The hires/source options went with it, and nothing has carried them since — git log -S hires --all returns only #381, #771 and #1412. First affected release is v0.19.27 (2025-02-12).

So this is not a testing-path gap; both paths lost the same fix. Coverage is simply where it is loudest, since the same transform feeds Vite's sourcemap chain during builds.

Upstream

I've filed the root cause at unjs/unimport#562 — the bare s.generateMap() at src/unplugin.ts:64 is still there on main, so every unplugin consumer is affected, not just WXT. If unimport takes the default change, whatever WXT does here can be removed again later.

Suggested fix

Restore the wxt:unimport plugin with #381's sourcemap options, keeping the two things #1412 actually needed: enforce: 'post' and a regex include/exclude filter (the @vitejs/plugin-vue case), with the defaults imported from unimport/unplugin rather than copied so they can't drift. One plugin, shared by the build path and WxtVitest(), so both are fixed by the same change.

Reproduction

Any WXT project with WxtVitest() in vitest.config.ts and coverage.include: ['src/**'].
Such as https://github.com/zizzfizzix/scrape-similar

Steps to reproduce
  1. Have a module that references an auto-imported global (browser, storage, …) and a test that renders/exercises it.
  2. vitest --run --coverage → that module reports ~1-3 measured statements at 100%.
  3. Edit node_modules/.pnpm/unimport@*/node_modules/unimport/dist/unplugin.mjs, changing map: s.generateMap() to map: s.generateMap({ hires: 'boundary', source: id }).
  4. Re-run → measured statements jump and real coverage appears.

Measured in my project:

map: value measured statements reported coverage
s.generateMap() (current) 871 100%
s.generateMap({ hires: true }) 3,390 90.02%
s.generateMap({ hires: 'boundary', source: id }) 3,390 90.02%

hires: 'boundary' matches hires: true exactly here and is the cheaper option — and it is already what #381 chose.

Reduced to a four-branch module using one auto-imported global, with a test covering a single branch:

Statements Branches
today 100% (0/0) — file absent from the report 100% (0/0)
with the fix 37.5% (3/8) 16.66% (1/6)
System Info
  • wxt 0.21.4 (broken since 0.19.27)
  • unimport 6.4.0 (transitive)
  • vitest 4.1.11, @vitest/coverage-v8 4.1.11 (reproduced identically with @vitest/coverage-istanbul, so it is not provider-specific)
  • vite 8.2.2
Used Package Manager

pnpm

Validations

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at the WxtVitest entry point and the shared Vite plugin setup, then compare the current integration with changes from #381 and #1412. Reproduce the issue with an auto-imported global and coverage enabled. Done means both build and test paths preserve source and column mappings and coverage reports the affected statements normally.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript, vite
Domain
build-system, devtools, testing-qa
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.