Pre-bundling workspace packages vs. native ESM resolution in pnpm monorepos
0 reputation · 16 Nov 2024, 08:18 UTC
0 reputation · 16 Nov 2024, 08:18 UTC
In a pnpm workspace monorepo, managing local package dependencies presents a trade-off between development server performance and HMR accuracy.
optimizeDeps.include to force esbuild to pre-bundle hoisted workspace packages into a single cache. While this significantly reduces cold start times for large dependency graphs, it risks capturing stale symlink targets when pnpm hoists packages, resulting in the server serving outdated code until the cache is manually invalidated.Alternatively, relying on server.deps.inline combined with resolve.preserveSymlinks allows Vite to resolve local packages as native ESM. This ensures that the latest source code is always served, but it can increase per-request resolution overhead and introduce noticeable HMR latency as the project grows.
uncertainty lies in the deterministic behavior of cache invalidation during package.json changes and how Vite interacts with the pnpm nested node_modules structure.
optimizeDeps.exclude for all workspace packages or selectively include them with a custom server.fs.allow list?35275 reputation · 16 Nov 2024, 08:37 UTC
For pnpm monorepos, it is more reliable to use optimizeDeps.exclude for all local workspace packages. This prevents esbuild from caching local source code into the .vite/deps directory, ensuring that Hot Module Replacement (HMR) reflects changes instantly without requiring manual cache invalidation or server restarts.
The choice between pre-bundling and native ESM resolution is a trade-off between initial load speed and developer experience (DX) reliability:
optimizeDeps.include): Reduces the number of HTTP requests by merging many small ESM files into a single bundle. However, because pnpm uses symlinks, Vite may fail to detect internal source changes within a workspace package, leading to stale code being served from the cache.optimizeDeps.exclude): Forces the browser to request each file individually. While this can cause a "request waterfall" that slows down the initial page load in massive projects, it guarantees that the latest source code is always served.To implement a reliable native ESM workflow for workspace packages, apply the following configuration in vite.config.ts:
export default defineConfig({
optimizeDeps: {
// Prevent local packages from being bundled into .vite/deps
exclude: ['@my-repo/shared-ui', '@my-repo/utils']
},
server: {
fs: {
// Allow Vite to serve files from the monorepo root
allow: ['..']
}
},
resolve: {
// Ensure symlinks are handled consistently
preserveSymlinks: true
}
});
This recommendation assumes the use of Vite 4.x or 5.x. In older versions, server.fs.allow was more restrictive, often requiring explicit paths to every symlinked directory. In modern versions, setting allow: ['..'] typically suffices to grant access to the workspace root.
Note on consistency: The interaction between pnpm symlinks and optimizeDeps has remained largely consistent across recent major versions, but the underlying mechanism for cache invalidation still primarily triggers on package.json changes. Since internal source edits do not change the package.json version, exclude remains the only deterministic way to avoid stale bundles.
To verify your configuration is working as intended:
/node_modules/.vite/deps/..., the package is still being pre-bundled.node_modules (or the symlinked path), native ESM resolution is active.Missing Diagnostic: Are you utilizing a build step (e.g., TypeScript to JS) for your workspace packages before Vite consumes them, or are you consuming raw .ts files?
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.