[Bug]: HMR throws "Module not found" error when dependency rebuilds in monorepo
Version
System:
OS: macOS 15.6.1
CPU: (14) arm64 Apple M4 Pro
Memory: 11.55 GB / 48.00 GB
Shell: 5.9 - /bin/zsh
Browsers:
Chrome: 140.0.7339.208
Edge: 140.0.3485.94
Safari: 18.6
Details
Description
In a monorepo environment, when using turbo watch to monitor dependency library source code changes and trigger automatic rebuilds, Rsbuild's HMR throws a Module not found error, while Vite behaves normally under the same conditions.
Rsbuild Environment Test Results
- ✅ When dependencies are built with tsup, HMR works correctly
- ❌ When dependencies are built with rslib, HMR throws errors and interrupts the development workflow
- ❌ When dependencies are built with tsdown, HMR throws errors and interrupts the development workflow
Vite Environment Test Results (Comparison)
- ✅ When dependencies are built with tsup, HMR works correctly
- ✅ When dependencies are built with rslib, HMR works correctly
- ✅ When dependencies are built with tsdown, HMR works correctly
Conclusion: With the same monorepo configuration and dependency build process, HMR works correctly for all dependency libraries in the Vite environment. The issue appears to be with Rsbuild.
Reproduction
I've created a minimal reproduction repository: issue-rsbuild-rsrepo
Steps to Reproduce
- Clone the repository and initialize:
git clone https://github.com/curlykay/issue-rsbuild-rsrepo.git
cd rsrepo
pnpm run setup
- Test Rsbuild (problematic):
pnpm run prt:dev-watch
- Modify
packages/lib-rslib/src/index.ts, HMR throws error ❌ - Modify
packages/lib-tsdown/src/index.ts, HMR throws error ❌ - Modify
packages/lib-tsup/src/index.ts, HMR works correctly ✅
- Test Vite (working):
pnpm run pvt:dev-watch
- Modify
packages/lib-rslib/src/index.ts, HMR works correctly ✅ - Modify
packages/lib-tsdown/src/index.ts, HMR works correctly ✅ - Modify
packages/lib-tsup/src/index.ts, HMR works correctly ✅
Expected Behavior
After the dependency library finishes rebuilding, Rsbuild should execute HMR correctly, updating the page content without errors (just like Vite's behavior).
Actual Behavior
When dependency libraries built with rslib or tsdown finish rebuilding, Rsbuild HMR throws an error:
playground-rsbuild-ts:dev: start building removed lib-rslib/dist/index.js
playground-rsbuild-ts:dev: error Build error:
playground-rsbuild-ts:dev: File: ./src/index.ts:1:1
playground-rsbuild-ts:dev: × Module not found: Can't resolve 'lib-rslib' in
'/Users/ck/Space/fiddle/rsrepo/packages/playground-rsbuild-ts/src'
playground-rsbuild-ts:dev: ╭─[10:9]
playground-rsbuild-ts:dev: 8 │ <div class="content">
playground-rsbuild-ts:dev: 9 │
playground-rsbuild-ts:dev: 10 │ <p>${getLibRsLib()}</p>
playground-rsbuild-ts:dev: · ───────────
playground-rsbuild-ts:dev: 11 │ <p>${getLibTsdown()}</p>
playground-rsbuild-ts:dev: 12 │ <p>${getLibTsup()}</p>
playground-rsbuild-ts:dev: ╰────
An error mask appears on the page, interrupting the development workflow.
Note: The first modification to lib-rslib may work correctly, but the second modification will always fail.
Workaround
Trigger a save in packages/playground-rsbuild-ts/src/index.ts to temporarily recover.
Environment
- Rsbuild: 1.5.13
- Node: 22.20.0
- Package Manager: [email protected]
- OS: macOS (Darwin 24.6.0)
- Monorepo Tool: Turborepo 2.5.8
Additional Context
The log message building removed lib-rslib/dist/index.js indicates that Rsbuild detected the dependency file change.
Since Vite behaves normally under the exact same monorepo environment and dependency build process, while Rsbuild only works correctly with tsup-built dependencies, the possible causes are:
-
Rsbuild HMR module resolution timing issue: After detecting file changes, Rsbuild may attempt to re-resolve modules before the dependency files are fully written, whereas Vite may have better waiting or retry mechanisms
-
Rslib/Tsdown build output strategy differences: These tools may adopt a "delete-then-write" or "multi-stage write" strategy, creating a brief file absence window during the update process, and Rsbuild's HMR happens to attempt module resolution during this window
-
Tsup atomic output: Tsup may use a faster or more atomic file writing approach (such as writing to a temporary file first, then renaming), ensuring that Rsbuild always reads complete files
-
Turborepo file watching event timing: Although Vite working normally suggests this is less likely, there may still be timing coordination issues between the file change events triggered by Turborepo and Rsbuild's handling logic
The specific cause requires further analysis. Thank you for helping diagnose this issue!
Reproduce link
https://github.com/curlykay/issue-rsbuild-rsrepo
Reproduce Steps
rsbuild:
- Run "pnpm run setup && pnpm run prt:dev-watch"
- Modify “packages/lib-tsup/src/index.ts”, observe terminal output and page refresh. Repeat modification twice.
- Modify “packages/lib-rslib/src/index.ts”, observe terminal output and page refresh. Repeat modification twice.
- Compare results
vite:
- Run "pnpm run setup && pnpm run pvt:dev-watch"
- Modify “packages/lib-tsup/src/index.ts”, observe terminal output and page refresh. Repeat modification twice.
- Modify “packages/lib-rslib/src/index.ts”, observe terminal output and page refresh. Repeat modification twice.
- Compare results
Thank you for your detailed explanation, I will research this issue
From my initial testing, this issue shows a mix of expected behavior and areas that could be improved:
- When you run
rslib buildmultiple times, Rslib clean thedistdirectory before generating new bundles. This is the expected behavior. If you want to avoid clearing the output each time, userslib build --watchinstead of running the build command repeatedly. - When the
distdirectory is cleaned, Rsbuild will report that it can't resolve thelib-rslib/dist/index.jsfile — this is also expected. - Once the bundles are re-generated in the
distdirectory, Rsbuild should recover from the error state and finish a successful build. However, this recovery doesn't currently work as expected, we'll look into it further.
Thank you for your prompt follow-up.
I attempted to find a temporary solution and successfully resolved the Hot Module Replacement (HMR) issue by enabling rslib build --watch mode. However, this requires some custom modifications to package.json and turbo.json. The specific changes can be referenced in this commit: https://github.com/curlykay/issue-rsbuild-rsrepo/commit/4254469f8fc51f4088e42a138056a492c38acb6d.
While effective, this solution comes with a minor drawback: to ensure watch mode functions correctly, I can no longer use ^build within dependsOn in turbo.json. This compromises Turborepo's ability to automatically analyze task dependencies. In my view, this stems primarily from the lack of an “exclude” capability in turbo.json's dependsOn configuration, making it less straightforward to have fine-grained control over dependencies.
Therefore, I eagerly anticipate the Rsbuild team's fix for this issue. Once Rsbuild can properly “recover from the error state and finish a successful build” I won't need to rely on these custom configurations and can organize my projects in a more standard and concise manner.
Makes sense. @stormslowly is already investigating the monorepo error recovery issue and hopes to resolve it soon. 😄
related to https://github.com/web-infra-dev/rspack/issues/11239
Currently workaround:
import { defineConfig } from "@rsbuild/core";
export default defineConfig({
tools: {
rspack: {
watchOptions: {
followSymlinks: true,
ignored: [],
},
},
},
});
the root cause is: https://github.com/web-infra-dev/rspack/issues/11239#issuecomment-3404402002
I will fix it later.
Useful configuration! It perfectly solved the problem—truly astonishing efficiency!
Aside:
When
/package/rslib/distis removed bynpm run build:rslib
When starting a Vite project (pnpm run pvt:dev-watch), I experimentally deleted only the packages/lib-rslib/dist directory manually. The Vite service showed no reaction—no error logs, no hot module replacement triggered. It only triggered HMR after modifying the source code in packages/lib-rslib/src/index.ts and regenerating the new dist files.
That said, this behavior actually provides a decent development experience. Whether this is Vite's intended mechanism, a deliberate optimization, or an accidental “bug” remains unclear to me at this point.
I experimentally deleted only the packages/lib-rslib/dist directory manually. The Vite service showed no reaction
This doesn't seem like the correct behavior, since it doesn't properly reflect the file changes.
You're right; the results do show that.
If I delete packages/lib-rslib/dist/index.js, the Vite service terminal immediately outputs an error log stating ‘file does not exist’. It's as if it deliberately ignores the deletion of the packages/lib-rslib/dist directory.