vite icon indicating copy to clipboard operation
vite copied to clipboard

Better (package-based) monorepo support

Open githorse opened this issue 1 year ago • 15 comments

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

githorse avatar Oct 12 '22 15:10 githorse

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.

vcaballero-salle avatar Dec 22 '22 10:12 vcaballero-salle

I'm very interested in this issue as well.

johnnyoshika avatar Dec 29 '22 05:12 johnnyoshika

~~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

space-nuko avatar Apr 06 '23 21:04 space-nuko

You can pay attention to this tool, which may solve your problem vite-run

biggerstar avatar Jul 28 '23 20:07 biggerstar

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 on library & library depends on library2, all existing in the same workspace, must have hot reload hmr
  • Vite only supports hmr for multiple modules by rewiring the paths

wakaztahir avatar Aug 28 '23 14:08 wakaztahir

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 avatar Sep 21 '23 07:09 jkhaui

@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?

cleferman avatar Nov 20 '23 13:11 cleferman

hope to see more progress on this.

XilinJia avatar Dec 23 '23 22:12 XilinJia

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).

samvv avatar Dec 29 '23 22:12 samvv

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.

appsforartists avatar Dec 29 '23 22:12 appsforartists

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.

AlwaysNoobCoder avatar Jan 19 '24 11:01 AlwaysNoobCoder

+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

roysandrew avatar May 09 '24 13:05 roysandrew

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 avatar Jul 17 '24 14:07 WadePeterson

@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.

geyang avatar Aug 08 '24 16:08 geyang

@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
    }
  };
});

WadePeterson avatar Aug 08 '24 17:08 WadePeterson