vite
vite copied to clipboard
Better (package-based) monorepo support
Description
The documentation on setting up a monorepo with Vite is pretty minimal. (Compare nx
's, for example.) I don't fully understand this sentence:
It will not attempt to bundle the linked dep, and will analyze the linked dep's dependency list instead.
More importantly, though, is this part:
When making changes to the linked dep, restart the dev server with the --force command line option for the changes to take effect.
Since most of my code is in the libraries, not the app, this means I'll have to reboot constantly, which would seem to defeat the purpose of Vite.
Is Vite not intended for monorepos? (Not for package-based monorepos?) Whether the answer is yes or no, could the documentation be expanded to better address the question? If the answer is yes, could we have some sort of --watch
feature that rebuilds on changes in linked packages in a monorepo?
Suggested solution
- Expand the documentation to better explain how (package-based) monorepos are intended to work with Vite.
- Provide a simple working demo/tutorial of Vite in a package-based monorepo. (Ideally, with a Typescript project -- getting Typescript sub-packages to work correctly with other build tools has been tricky, to say the least.)
- Implement a
--watch
or equivalent feature to automatically pick up changes to packages in a monorepo.
Additional context
My repository is a monorepo consisting of a few apps and a large set of libraries, each constituting a package:
root/
packages/
app1/
app2/
lib1/
lib2/
lib3/
Apps reference libraries by importing them in package.json
(and tsconfig.json
), and libraries import other libraries the same way, forming a (rather complicated) DAG.
Validations
- [X] Follow our Code of Conduct
- [X] Read the Contributing Guidelines.
- [X] Read the docs.
- [X] Check that there isn't already an issue that request the same feature to avoid creating a duplicate.
Hi, any news on this? I have the same concern about the build system picking up changes in libraries. I tried with nx
to no avail, now I was thinking of trying with vite
, but it seems that it won't work either.
I'm very interested in this issue as well.
~~Any news? I'm finding I have to transition away from Vite not just for a library with a demo app I'm making, but another app that depends on that library, both use Vite. If I can't import the in-progress library from my main app and work on the library with hot reloading then I'm stuck.~~
EDIT: Actually I figured it out, I tried using Turborepo alongside Vite and got the results I wanted using a monorepo structure, it works with hot reloading too. So it sounds like a documentation issue if they can describe how to use Turborepo
You can pay attention to this tool, which may solve your problem vite-run
This is needed, I have a yarn workspaces repo, There are two packages, demo
& lib
, lib package has tsc --watch
and other stuff, whenever I change source code in lib
package, instead of a hot reload, I get a full page refresh, which becomes a little annoying.
-
Vite does not support
hmr
for monorepo packages -
Vite should also support multiple interdependent packages, for example,
demo
package depends onlibrary
&library
depends onlibrary2
, all existing in the same workspace, must have hot reloadhmr
- Vite only supports hmr for multiple modules by rewiring the paths
Very surprised this hasn't received more attention considering that Vite is ostensibly all about dev ergonomics? I mean, I'm sure the team has their hands full but this seems like it'd be a pretty standard way to set up a modern-day monorepo.
FYI I'm probably just salty so ignore me 😬 I spent a day setting up an nx/pnpm monorepo with a Vite TS lib mode template (for shipping npm packages), started building a complex project on top, and just assumed hot reloading wasn't working because I'd misconfigured something which could be easily fixed later.
This is the first I'm seeing that hmr/fast refresh isn't supported at all by Vite when used in monorepos lol. A workaround I'm about to try is enabling rollup's build watcher with config.build.watch
🤔
edit: Ok I did manage to get hmr working, in my case it was very simple but also took a while to figure out due to recent changes in Vite's module resolution mechanism: https://github.com/vitejs/vite/issues/11114
Problem
I had copy + pasted the default lib mode settings provided in the docs: https://vitejs.dev/guide/build.html#library-mode. Specifically, Vite suggests the following package.json
setup:
{
"name": "my-lib",
"type": "module",
"files": ["dist"],
"main": "./dist/my-lib.umd.cjs",
"module": "./dist/my-lib.js",
"exports": {
".": {
"import": "./dist/my-lib.js",
"require": "./dist/my-lib.umd.cjs"
}
}
}
The problem is that in a dev environment, the exports..import
field should really point to a raw uncompiled/untranspiled ESM file such that Vite's hmr feature works as it would in a non-monorepo environment. However, the "recommended" defaults in the docs leads us to believe that consuming the built output in /dist
is the only way to develop in a package-based setup, and of course hmr won't work in this context.
So, if my understanding is correct, pointing the import
field in exports
to your package's source file (instead of its build artifact) fixes the problem and hmr works as expected (I've also removed the cjs build in the example below):
{
"name": "my-lib",
"type": "module",
"files": ["src", "dist"],
"main": "./dist/my-lib.js",
"module": "./dist/my-lib.js",
"source": "./src/my-lib.mjs",
"exports": {
".": {
"import": "./src/my-lib.mjs",
}
}
}
@jkhaui thanks, works like a charm. I don't fully understand the implication of this setting (if there is any).
"exports": {
".": {
"import": "./src/my-lib.mjs",
},
"./sub-package": {
"import":"../sub-package/src/sub-package.mjs"
}
}
From what I gather, this setting just points the import to the entry file. From the snippet I linked this would mean we have can have:
import {smth} from 'my-lib'
and
import {smthElse} from 'my-lib/sub-package
Is that correct?
And what's the impact of this setting once the library is built?
hope to see more progress on this.
I just opened a discussion to something that is somewhat related to this: https://github.com/vitejs/vite/discussions/15466
Basically I'm wondering if Vite could support something like Parcel's source
field in package.json
. The source
field is what Parcel uses to find a suitable src/index.js
or src/index.ts
, while the main
field can point to a prebundled version made with whatever tool you prefer.
This way, even though your main
points to a bundle, Vite will support hot reloading of the entire library in the monorepo without the developer having to rebuild the sources each time.
This sounds like an awesome feature to have (if it doesn't exist already).
The issue @samvy just linked has another workaround: overloading envDir
to serve as the root of a monorepo, so all the files therein get watched for changes.
I am a Java Developer.
I used to hate the Maven as build tool in java ecosystem.
but ..... after I step into web world recently...
I swear to god, maven can't be more cute !
the tooling and chaos in web, holy fack... can't believe it.
+1 - there is, as far as I am aware, no path to package-based monorepos that make use of peer dependencies to work without significant compromise of dev ergonomics in vite. The canonical solution of using injected dependencies, because of the 'node_modules' heuristic for determining whether to care about changes while the dev server is running basically kills any chance of hot module reloading.
There is at least some community, and industry movement to build tooling and workflows to support this: https://github.com/microsoft/rushstack/blob/main/common/docs/rfcs/rfc-4230-rush-subspaces.md https://github.com/tiktok/pnpm-sync/tree/main
being the two efforts I think that have the most impetus behind them at present. Is it something that the vite team have already got a plan for it/have ruled it out?
I started a discussion some months ago to see if there was some alternate path and the solution eventually involved horrible hacks around symlinked packages - https://github.com/vitejs/vite/discussions/14672
One alternative to updating the package.json to point to the source files, is to use the resolve.alias option in the Vite config to map the package names to the source files.
e.g., with the following package.json
{
"name": "my-lib",
"type": "module",
"exports": {
".": {
"import": "./dist/my-lib.js",
}
}
}
You could have this in your vite.config.
export default defineConfig({
// ...
resolve: {
alias: {
'my-lib': path.resolve(__dirname, 'packages/my-lib/src/my-lib.mjs')
}
}
})
This is a basic example with a package that contains just a single root export. For packages with more complex exports, you should be able to use Regex based aliases
@WadePeterson How do you handle peer dependencies that you want to externalize?
For example, a few libraries require context providers, so a singleton need to be referenced by both the module that depends on it, and the main app.
Right now it seems the aliased resolver will use the local node_modules
version of the peer dependency during compilation as opposed to the global one.
@WadePeterson How do you handle peer dependencies that you want to externalize?
For standard build, you should be able to configure build.rollupOptions
using external
and output.globals
e.g.:
const globals = {
react: 'React',
'react-dom': 'ReactDOM',
};
export default defineConfig({
// ...
build: {
rollupOptions: {
external: Object.keys(globals), // ['react', 'react-dom']
output: {
format: 'iife',
name: 'MyBundle',
globals,
},
},
},
});
This doesn't apply to Vite in serve
mode (i.e. local dev), for non-ESM bundles. For that, I tried various plugins, but found vite-plugin-external
did exactly what I needed. I use it like this:
import createExternal from 'vite-plugin-external';
const globals = {
react: 'React',
'react-dom': 'ReactDOM',
};
export default defineConfig(({command}) => {
return {
// ...
plugins: [
// ... other plugins
// only add this in dev server mode
...(command === 'serve' ? [createExternal({externals: globals})] : []),
],
build: {
// ... same build config as above, though `external`/`globals` from that don't actually apply in dev server
}
};
});