Skip to content

A chained command is split into independently cached sub-tasks, but output stays task-level — a hit replays a stale output over a fresh one #589

Description

@jderboven

Summary

When a task's command chains several commands (a && b, or an array), Vite+ caches each sub-command independently, but the task declares a single, task-level output. Every sub-task therefore claims every output file.

On a partially-cached run this is unsound: a sub-task that hits restores its archived copy of the shared outputs over the file a sub-task that missed has just regenerated. The task exits 0 and the artifact on disk is stale.

Ordering makes it visible: with 3 hits and 1 miss on the same task, the missing sub-task in position 2 of 4 leaves a stale output, while the same sub-task in position 4 of 4 leaves the correct one. Clearing the cache (no replay at all) is always correct.

This is not an input-declaration problem. Fixing input so the miss fires correctly does not fix it: the sub-task runs, writes the right file, and a later hit overwrites it.

Reproduction

mkdir vp-chained-output && cd vp-chained-output
npm init -y
npm i -D vite-plus@0.2.7
mkdir -p out

vite.config.ts:

import { defineConfig } from 'vite-plus';

export default defineConfig({
  run: {
    tasks: {
      gen: {
        command: [
          'node -e "require(\'fs\').writeFileSync(\'out/a.txt\', require(\'fs\').readFileSync(\'src-a.txt\',\'utf8\'))"',
          'node -e "require(\'fs\').writeFileSync(\'out/b.txt\', require(\'fs\').readFileSync(\'src-b.txt\',\'utf8\'))"',
        ],
        output: ['out/**'],
      },
    },
  },
});

Automatic tracking matters here: it is what gives each sub-task a different input set, so one can hit
while another misses. With a single explicit input list, every sub-task invalidates together and the bug
stays hidden.

echo v1 > src-a.txt && echo v1 > src-b.txt
npx vp run gen          # miss, out/a.txt = v1, out/b.txt = v1
npx vp run gen          # hit

echo v2 > src-a.txt     # only the first sub-command's input changes
npx vp run gen          # -> "cache miss: 'src-a.txt' modified" for sub-task 1
                        # -> "cache hit"                        for sub-task 2

cat out/a.txt           # expected v2 - observed v1, exit code 0

Observed exactly as above on vite-plus 0.2.7.

Expected

Each sub-task restores only the outputs it produced, or the task is treated as a single cache unit.

Actual

The archive of a hitting sub-task contains files written by its siblings and replays them, silently reverting fresher output. Exit code is 0.

Why it matters in practice

In our monorepo the pattern is common: a generator followed by a fixer, e.g.

kubb generate ./swagger.yml -c=kubb.config.mock.js && npm run fix-generated-mock-imports

Both write under src/gen/mocks/**, which is the task's only declared output. A partial hit yields a half-regenerated SDK with no signal. We found 14 tasks in this shape and had to disable caching on all of them.

Splitting them into one task per command — the obvious workaround, and the one we could apply to a Panda ship chain — is not possible when the second command fixes the output of the first: the fixer has the same files as input and output, so the tracker declines to cache it (Not cached: read and wrote …).

Suggested directions

  • track outputs per sub-task rather than per task, so a sub-task only restores what it wrote; or
  • treat a task with a chained command and more than one output as a single cache unit; or
  • at minimum, refuse to cache (or warn) when a task chains commands and declares outputs that several sub-tasks can write — a loud refusal is far better than a silent stale artifact.

Environment

  • vite-plus 0.2.7
  • macOS darwin 25.5.0, arm64; Node 22.22.0, pnpm 9.15.0

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions