TIA's JS module graph is always empty for Vue projects (rolldown cannot parse SFCs)
TIA's JS module graph is always empty for Vue projects (rolldown cannot parse SFCs)
What happens
pest-tia-vite-deps.mjs builds the JS module graph by calling rolldown() directly with three internal plugins (externalBare, assetStub, collector). .vue is listed in both PAGE_EXTENSIONS and resolve.extensions, so those files are collected as entries and handed to rolldown.
rolldown has no built-in SFC handling, so every .vue entry fails at the first byte of <script setup>:
[PARSE_ERROR] Unexpected token
╭─[ resources/js/Pages/Example.vue:1:1 ]
1 │ <script setup>
│ ┬On a Vue application with ~150 pages, every entry fails.
Why it is silent
In a normal install, loadRolldown() returns null (rolldown is not a dependency of Vite 6/Rollup), so main() takes this path:
const loaded = await loadRolldown()
if (loaded === null) {
process.stdout.write('{}') // line 260
return
}It writes {} and exits 0. Pest records an empty JS graph and reports success, so TIA looks healthy while no resources/js/** change can ever select a test. There is no warning and no non-zero exit.
Backend coverage is unaffected, the PHP graph is populated correctly (a representative test of ours has 136 edges through middleware, services, models and routes). The gap is specifically frontend-only changes.
Installing rolldown makes it worse, not better
The comment at loadRolldown() suggests a rolldown-vite install is the supported route:
// rolldown-vite installs (vite@npm:rolldown-vite) ship rolldown as a
// dependency of the vite package rather than a top-level install.That does fix resolution. I verified both lookup paths resolve with [email protected] (bundling [email protected]). But it does not fix parsing. Once rolldown resolves, the silent {} turns into a hard exit 1 that fails the whole run, because the parse error is now reached.
So on a Vue project there are only two states: empty graph (no rolldown) or failed run (rolldown present). Neither produces a usable JS graph.
Reproduction
Minimal repro, independent of Pest. It replicates the exact rolldown() options from pest-tia-vite-deps.mjs:306-318:
// probe.mjs, run with: node probe.mjs
import { createRequire } from 'node:module'
import { resolve } from 'node:path'
const PROJECT = process.cwd()
const r = createRequire(new URL('./package.json', import.meta.url))
const { rolldown } = await import(r.resolve('rolldown'))
const bundle = await rolldown({
input: { p0: resolve(PROJECT, 'resources/js/Pages/Example.vue') },
cwd: PROJECT,
resolve: {
alias: { '@': resolve(PROJECT, 'resources/js') },
extensions: ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.json', '.vue'],
},
transform: { jsx: 'preserve' },
treeshake: false,
plugins: [],
logLevel: 'silent',
onLog: () => {},
})
await bundle.generate({ format: 'esm' })Any .vue file containing <script setup> reproduces it.
Environment
| Pest | 5.1.2 |
| Vue | 3.5.x |
| Vite | 6.4.3 (also probed against [email protected]) |
| rolldown | 1.0.0-beta.53 (bundled with rolldown-vite) |
| Node | 22 |
Related
- #1917, the documented CI baseline command records no graph, and unlocking the guard records an under-selecting one. Same failure shape: TIA reports success while the recorded graph cannot select the affected tests.
Happy to help test a fix.
Source: pestphp/pest