Skip to content

Persistent cache stops writing in watch mode after cache directory is removed #14723

Description

@hardfist

Version

  • @rspack/core: 2.1.3
  • @rspack/cli: 2.1.3
  • Node: v24.16.0
  • npm: 11.13.0
  • Platform: darwin arm64

Details

When cache: { type: "persistent" } is enabled and rspack build --watch is already running, deleting node_modules/.cache/rspack externally causes later rebuilds to keep compiling successfully, but persistent cache pack files are no longer written for the rest of that watch session.

Restarting the watch process makes persistent cache files generate again.

Expected behavior: after a rebuild, Rspack should either recreate the persistent cache pack files or surface a cache write error. It should not silently keep the watch build alive with an empty/non-writing persistent cache directory.

Reproduce link

No external repo. Minimal copy-paste repro below.

Reproduce Steps

Create these two files, then run npm install && npm test.

package.json

{
  "name": "rspack-watch-persistent-cache-delete-repro",
  "private": true,
  "version": "1.0.0",
  "scripts": {
    "test": "node repro.js"
  },
  "devDependencies": {
    "@rspack/cli": "2.1.3",
    "@rspack/core": "2.1.3"
  }
}

repro.js

const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { spawn } = require("node:child_process");

const root = fs.mkdtempSync(path.join(os.tmpdir(), "rspack-watch-cache-"));
const srcDir = path.join(root, "src");
const cacheRoot = path.join(root, "node_modules/.cache/rspack");

fs.mkdirSync(srcDir, { recursive: true });
fs.writeFileSync(path.join(srcDir, "index.js"), "console.log(1);\n");
fs.writeFileSync(
  path.join(root, "rspack.config.js"),
  `module.exports = {
  mode: "development",
  entry: "./src/index.js",
  output: { path: __dirname + "/dist", filename: "main.js" },
  cache: { type: "persistent" }
};
`
);

const rspackBin =
  process.env.RSPACK_BIN ||
  path.join(
    __dirname,
    "node_modules/.bin",
    process.platform === "win32" ? "rspack.cmd" : "rspack"
  );

function listCacheFiles() {
  if (!fs.existsSync(cacheRoot)) return [];

  const files = [];
  const walk = (dir) => {
    for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
      const file = path.join(dir, entry.name);
      if (entry.isDirectory()) {
        walk(file);
      } else {
        files.push(path.relative(cacheRoot, file));
      }
    }
  };
  walk(cacheRoot);
  return files.sort();
}

function waitForCompile(getCompileCount, expectedCount, output) {
  return new Promise((resolve, reject) => {
    const startedAt = Date.now();
    const timer = setInterval(() => {
      if (getCompileCount() >= expectedCount) {
        clearInterval(timer);
        resolve();
        return;
      }

      if (Date.now() - startedAt > 30000) {
        clearInterval(timer);
        reject(
          new Error(
            `Timed out waiting for compile ${expectedCount}; saw ${getCompileCount()}.\n` +
              output().slice(-2000)
          )
        );
      }
    }, 100);
  });
}

async function main() {
  console.log(`Temporary project: ${root}`);
  console.log(`Rspack binary: ${rspackBin}`);

  let output = "";
  let compileCount = 0;
  const child = spawn(rspackBin, ["build", "--watch", "-c", "rspack.config.js"], {
    cwd: root,
    env: process.env
  });

  const observe = (chunk) => {
    const text = chunk.toString();
    output += text;
    process.stdout.write(text);
    const matches = output.match(/Rspack compiled successfully/gi);
    compileCount = matches ? matches.length : 0;
  };

  child.stdout.on("data", observe);
  child.stderr.on("data", observe);

  try {
    await waitForCompile(() => compileCount, 1, () => output);
    await new Promise((resolve) => setTimeout(resolve, 500));

    const initialFiles = listCacheFiles();
    console.log(`Initial persistent cache files: ${initialFiles.length}`);
    if (initialFiles.length === 0) {
      throw new Error("Initial watch build did not create persistent cache files.");
    }

    fs.rmSync(cacheRoot, { recursive: true, force: true });
    console.log("Deleted node_modules/.cache/rspack while watch is still running.");

    fs.writeFileSync(path.join(srcDir, "index.js"), "console.log(2);\n");
    await waitForCompile(() => compileCount, 2, () => output);
    await new Promise((resolve) => setTimeout(resolve, 1000));

    const filesAfterRebuild = listCacheFiles();
    console.log(`Persistent cache files after rebuild: ${filesAfterRebuild.length}`);

    if (filesAfterRebuild.length === 0) {
      throw new Error(
        "BUG: rebuild succeeded, but persistent cache pack files were not recreated."
      );
    }
  } finally {
    child.kill("SIGTERM");
  }
}

main().catch((error) => {
  console.error(error.stack || error);
  process.exitCode = 1;
});

Actual Output

Temporary project: /var/folders/.../T/rspack-watch-cache-I1MAnJ
Rspack binary: /.../node_modules/.bin/rspack
Rspack compiled successfully in 45 ms
Initial persistent cache files: 11
Deleted node_modules/.cache/rspack while watch is still running.
Rspack compiled successfully in 8 ms
Persistent cache files after rebuild: 0
Error: BUG: rebuild succeeded, but persistent cache pack files were not recreated.

Notes

A control run without deleting node_modules/.cache/rspack shows that watch rebuilds do update persistent cache files. The failure only appears after the cache directory is removed while the watch process is still alive.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions