Auto-imports emit a line-only sourcemap, silently collapsing code coverage (regression since v0.19.27, affects build and test)
Nobody has claimed this yet.
- 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
- Have a module that references an auto-imported global (
browser,storage, …) and a test that renders/exercises it. vitest --run --coverage→ that module reports ~1-3 measured statements at 100%.- Edit
node_modules/.pnpm/unimport@*/node_modules/unimport/dist/unplugin.mjs, changingmap: s.generateMap()tomap: s.generateMap({ hires: 'boundary', source: id }). - 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
- Read the Contributing Guidelines.
- Read the docs.
- Check that there isn't already an issue that reports the same bug to avoid creating a duplicate.
- Check that this is a concrete bug. For Q&A open a GitHub Discussion or join our Discord Chat Server.
- The provided reproduction is a minimal reproducible example of the bug.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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