Skip to content

gsmeira/vite-plus-cache-repro

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

vite-plus task-cache repro — emptyOutDir delete-probe miscounted as a read

Minimal reproduction for a Vite+ task-cache bug: a vp build task whose build.outDir is nested under a tracked directory (e.g. laravel-vite-plugin's public/build) is never cached on any rebuild where a prior build is present.

The cause is in Vite+'s task-cache fs tracker — not in Vite, Rolldown, or Laravel (this repo uses none of them).

TL;DR

Two identical apps; the only difference is build.outDir:

App build.outDir Re-run over prior output
apps/control dist/ (default) 100% cache hit
apps/bug public/build/ (nested under the tracked public/) 0% — Not cached: read and wrote 'apps/bug/public/build/manifest.json'

Reproduce

pnpm install

# CONTROL — caches:
vp cache clean && vp run --filter control build   # build #1 (writes output)
vp cache clean && vp run --filter control build   # build #2 (executes over prior output, repopulates cache)
vp run --filter control build && vp run --last-details   # -> 100% cache hit

# BUG — never caches:
vp cache clean && vp run --filter bug build       # build #1
vp cache clean && vp run --filter bug build       # build #2 (executes over prior output)
vp run --filter bug build && vp run --last-details       # -> 0%, "read and wrote …/public/build/manifest.json"

The second vp cache clean (keeping the output on disk) is what arms the case: it forces build #2 to execute while a prior build is present, so emptyOutDir performs its recursive delete.

Expected vs actual

  • Expected: the bug app caches on re-run, exactly like control — nothing in the build genuinely reads its own output.
  • Actual: the bug app reports not cached because it modified its input, blaming a stable-named output file under public/build/ (manifest.json, index.html, or a copied public/ asset — varies by run).

Root cause

With a prior build present, Vite's emptyOutDir recursively deletes the old output. Node's recursive remove probes each existing file with openat(path, O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY), which fails with ENOTDIR on a regular file, then unlinks it. The task-cache fs tracker counts that failed O_RDONLY probe as a content read; the build then writes a fresh same-named file, so the task is flagged as having read and wrote the same path → self-modified input → uncacheable.

strace -f -e trace=%file of the bug build (over prior output) — the only read-like syscall on the manifest is the delete-probe; there is no content read:

lstat(".../public/build/manifest.json") = 0
openat(".../public/build/manifest.json", O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY) = -1 ENOTDIR
unlink(".../public/build/manifest.json") = 0
# ...later, in a worker thread, the fresh write:
openat(".../public/build/manifest.json", O_WRONLY|O_CREAT|O_TRUNC) = 29

control caches because its manifest lands in the default dist/, which the tracker excludes from input scope, so the identical probe is ignored. bug differs only in that public/build/ is nested under public/ — a directory the build reads from (it copies public/robots.txt), so the tracker treats it as input scope and the probe under it is counted.

What fixes it (fix localized to input-side exclusion)

Tested on the bug app, building over prior output:

Build-task config Result
baseline (this repo) ❌ 0% — read and wrote …/public/build/…
exclude the outDir from input: input: [{ auto: true }, "!public/build/**"] 100% cache hit
declare output: output: ["public/build/**"] ❌ 0% — still read and wrote …

So the cure is input-side exclusion of the output dir, not output declaration. (build.emptyOutDir: false — no recursive delete, hence no probe — also makes it cache, but leaves stale hashed assets un-pruned.)

Workaround

Add an input exclusion for the output dir to the build task — keeps emptyOutDir: true (output still pruned) and caches:

run: { tasks: { build: { command: "vp build", cache: true, input: [{ auto: true }, "!public/build/**"] } } }

Why this is a Vite+ issue (not upstream)

The recursive delete and the layout are both legitimate (Vite's emptyOutDir, a custom outDir). The defect is purely in Vite+'s task-cache fs tracker classifying an ENOTDIR-failing O_DIRECTORY delete-probe as a content read — proprietary Vite+ code with no equivalent in Vite/Rolldown. This repo reproduces it with no framework, no Laravel, no Rolldown-specific config.

Related

  • voidzero-dev/vite-plus#1187, voidzero-dev/vite-plus#1095 — same "task modifies its own input" class, fixed for the default output dir; this is the residual case for a custom outDir nested under a tracked dir.
  • voidzero-dev/vite-plus#1774 (PR, auto output + tracked envs) — routes Vite's real inputs/outputs to the runner via ignoreInput/ignoreOutput; that ignoreInput channel is exactly the input-side exclusion shown above to fix this, so it likely resolves this case.

System info

vp v0.1.12
Local vite-plus: 0.2.1   (vite 8.0.16, rolldown 1.1.1)
Node.js 24.17.0
pnpm 11.8.0
Linux

About

Minimal reproduction: vite-plus task cache never hits when build.outDir is nested under a tracked dir (e.g. Laravel public/build) — emptyOutDir's ENOTDIR delete-probe is miscounted as a content read

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors