vitejs / vite

Next generation frontend tooling. It's fast!
http://vitejs.dev
MIT License
67.09k stars 6.02k forks source link

Better (package-based) monorepo support #10447

Open githorse opened 1 year ago

githorse commented 1 year ago

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

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

vcaballero-salle commented 1 year ago

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.

johnnyoshika commented 1 year ago

I'm very interested in this issue as well.

space-nuko commented 1 year ago

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

biggerstar commented 1 year ago

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

wakaztahir commented 1 year ago

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.

jkhaui commented 11 months ago

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",
    }
  }
}
cleferman commented 9 months ago

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

XilinJia commented 8 months ago

hope to see more progress on this.

samvv commented 8 months ago

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

appsforartists commented 8 months ago

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.

AlwaysNoobCoder commented 7 months ago

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.

roysandrew commented 4 months ago

+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

WadePeterson commented 1 month ago

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

geyang commented 1 month ago

@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 commented 1 month ago

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