Building a Monorepo with Yarn and Vite

34 minute read     Updated:

Aniket Bhattacharyea %
Aniket Bhattacharyea

The article provides a guide on setting up a monorepo with Yarn and Vite. If you’re working in a monorepo, you might be interested in Earthly. Earthly significantly speeds up build times for complex projects. Check it out.

In a real-world project, you’ll often have many independent components that depend on each other. Organizing these components while keeping in mind the ease of development and deployment is a daunting task. Thankfully, with monorepos, all the related subprojects are contained within one single repository. This makes it easy for teams to collaborate and update the dependencies with new changes without the fear of breaking projects that rely on them.

In the JavaScript world, Yarn is a well-known package manager. With the recent addition of the workspaces feature, Yarn can now be used as a monorepo build tool. With workspaces enabled, Yarn can identify workspaces in a project, efficiently install and manage dependencies, handle dependencies between subprojects, and enable parallel builds.

When it comes to frontend development, Vite is a next-generation tool that’s designed to streamline development using JavaScript and TypeScript. Vite simplifies the creation of development environments for popular frontend frameworks like Vue, React, and Svelte, offering fast server start and hot module replacement (HMR) out of the box.

In this article, you’ll learn how to use Vite and Yarn workspaces to build a monorepo.

What Is Vite?

Traditionally, ECMAScript modules (ESM) were not supported by any browsers. This means developers needed to use bundlers such as webpack, Rollup, or Parcel to bundle all the source modules into JavaScript files that the browsers could run. However, this approach has a few performance drawbacks. For instance, when starting the dev server for the first time, the bundler must crawl through all the source code to create the bundle. If you have a large project with a bunch of source modules, this process can be very lengthy. Additionally, when one of the modules changes, the bundle must be reconstructed to reflect the changes, which is another time-consuming process.

Vite aims to improve upon these factors. It intelligently utilizes the native ESM capabilities of modern browsers to provide a fast server start as well as HMR, which lets you instantly reload a changed module without affecting the rest of the modules.

Vite has out-of-the-box support for TypeScript, JSX, and CSS, as well as support for frameworks like Vue, React, and Svelte. Thanks to its plugin architecture, you can easily customize its functionalities.

Creating a Monorepo with Yarn and Vite

In this article, you’ll learn how to build a monorepo with Yarn workspaces and Vite. The monorepo here will represent an internal portal for a company. In this scenario, there’s a portal that teams use, a separate portal for the managers, and another portal for admins. For simplicity, these portals only display a message, but you’re welcome to expand upon the concepts shown in this tutorial and build something more complex.

To demonstrate the flexibility of Vite, you’ll use both React and Vue, and the monorepo will have four subprojects:

  1. A teams app made in React
  2. A managers app made in React
  3. A Vue app named admins
  4. A React library named common-ui (both the teams and managers apps will have this library as a dependency)
Architectural diagram courtesy of Aniket Bhattacharyea

To follow along, make sure you have Node.js 18+ installed.

Creating the Monorepo Structure

The first thing you need to do is enable Yarn by running corepack enable. Then, create a directory and initialize a Yarn project in it:

mkdir yarn-vite-monorepo && cd yarn-vite-monorepo
yarn init -2

Next, you need to create the directory structure of the monorepo. The applications will go under the apps directory, and the library will go under the packages directory. Use the following command to create the apps and packages directories:

mkdir apps packages

Then, add the workspaces field to the package.json file at the root of the project:

{
    ...
    "workspaces": [
        "packages/*",
        "apps/*"
    ]
}

This field tells Yarn that all the directories under apps and packages should be considered workspaces.

Creating the Apps

To create the apps, navigate into the apps directory:

cd apps

Then, create two React apps (the teams and managers portals):

yarn create vite teams --template react
yarn create vite managers --template react

Next, create the Vue app (the admins portal):

yarn create vite admins --template vue

Once you’ve created all three portals, navigate to the packages directory:

cd ../packages

And create a package named common-ui:

yarn create vite common-ui --template react

Then, navigate to the root of the project and run yarn. This will install the dependencies for each of the workspaces. Note that there’s no need to individually install dependencies for the workspaces. When you run yarn in a project with workspaces enabled, Yarn automatically installs dependencies for each workspace.

Creating the Shared Library

To create the shared library, you’ll be working in the packages/common-ui directory.

Delete all the files in the src directory, then create a new file named banner.jsx with the following code:

export default function Banner({ instanceName }) {
    return <h1>Welcome to the {instanceName} portal</h1>;
}

This is a simple React component that displays a message on the screen. This component will be used in both the managers and teams apps.

You need to export this React component so that it can be imported into the apps. Create a new folder named lib and add a file named main.js to this directory. Paste the following code into it:

export { default as Banner } from '../src/banner'

This file simply exports the Banner component and acts as an entry point for the library.

Next, you need to let Vite know how to build this project as a shared library. Open the vite.config.js file and replace the existing code with the following:

import react from "@vitejs/plugin-react";
import { resolve } from "path";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [react()],
  build: {
    lib: {
      entry: resolve(__dirname, "lib/main.js"),
      name: "common-ui",
      fileName: "common-ui",
    },
    rollupOptions: {
      external: ["react", "react-dom"],
      output: {
        globals: {
          react: "React",
          "react-dom": "React-dom",
        },
      },
    },
  },
});

The important part of this code is that the build entry tells Vite to build the library using lib/main.js as the entry point. The compiled file will be stored at dist/common-ui.js and dist/common-ui.umd.cjs.

Now, open the package.json file and add the following entries:

{
    ...
    "files": [
        "dist"
    ],
    "main": "./dist/common-ui.umd.cjs",
    "module": "./dist/common-ui.js",
    "exports": {
        ".": {
        "import": "./dist/common-ui.js",
        "require": "./dist/common-ui.umd.cjs"
        }
    },
}

These entries export the compiled JavaScript files and are responsible for finding the component when you import it into the apps later.

Navigate to the root of the project and build the common-ui library:

yarn workspace common-ui build

This command executes the build script in the common-ui workspace. The build script runs vite build and compiles the component using the previous configuration. You should get an output that looks like this:

vite v5.0.11 building for production...
10 modules transformed.
dist/common-ui.js  21.20 kB │ gzip: 6.32 kB
dist/common-ui.umd.cjs  13.94 kB │ gzip: 5.44 kB
built in 172ms

Completing the React Apps

To finish setting up the React apps, you need to install the common-ui library as a dependency in the teams and managers apps. Since the common-ui library is part of the same monorepo, you’ll use a special syntax for referencing it.

Open the package.json file in both apps/teams and apps/managers, and add the following entry in the dependencies object:

"common-ui": "workspace:^",

From the root of the project, run yarn to install the dependency.

You’re now ready to use the exported component in your React apps. Open teams/src/App.jsx and replace the existing code with the following:

import { Banner } from 'common-ui'

function App() {

  return (
    <>
      <Banner instanceName="Teams" />
    </>
  )
}

export default App

Note that you’re importing Banner from common-ui just like a usual library. Yarn takes care of linking the dependency behind the scenes.

Open managers/src/App.jsx and replace the existing code with the following:

import { Banner } from "common-ui"

function App() {

  return (
    <>
      <Banner instanceName="Managers" />
    </>
  )
}

export default App

You can now run the apps and see if they work. From the root of the project, start the teams app:

yarn workspace teams dev

If you visit http://localhost:5173 in your browser, you should see the following:

The Teams portal

Stop the server and run the managers app:

yarn workspace managers dev

Visit http://localhost:5173 again, and you should see the managers app:

The Managers portal

Completing the Vue App

After completing the React apps, you need to finish the Vue app. Open apps/admins/src/components/HelloWorld.vue and replace the code with the following:

<script setup>

defineProps({
  instanceName: String,
})

</script>

<template>
  <h1>Welcome to the  portal</h1>

</template>

This code creates a component analogous to the Banner component.

Open apps/admins/src/App.vue and replace all the code with the following:

<script setup>
import HelloWorld from './components/HelloWorld.vue'
</script>

<template>
  <HelloWorld instanceName="Admins" />
</template>

From the root of the project, start the admins app:

yarn workspace admins dev

Visit http://localhost:5173. This time, you’ll see the Vue app:

The Admins portal

Note: You don’t necessarily need to run the dev scripts from the root of the project. You can also run a script from within a workspace that defines it using the yarn run command, just like a usual Yarn project. For example, you can run yarn run dev from within the apps/admins directory instead of running yarn workspace admins dev from the project’s root.

Enabling Parallel Execution

So far, you’ve run the apps individually. Similarly, if you want to build the apps, you can run the build scripts individually. But there’s a better way! If you want to run the same Yarn script for all the workspaces, you can use the yarn workspaces foreach command. This command runs the same script in each workspace.

For example, using the following code, you can run the build script for all the workspaces:

yarn workspaces foreach --all -pt run build

You should see an output like this:

[admins]: Process started
[common-ui]: Process started
[common-ui]: vite v5.0.11 building for production...
[common-ui]: transforming...
[common-ui]: ✓ 10 modules transformed.
[common-ui]: rendering chunks...
[common-ui]: computing gzip size...
[common-ui]: dist/common-ui.js  21.20 kB │ gzip: 6.32 kB
[common-ui]: dist/common-ui.umd.cjs  13.94 kB │ gzip: 5.44 kB
[common-ui]: ✓ built in 183ms
[common-ui]: Process exited (exit code 0), completed in 0s 496ms
[admins]: vite v5.0.11 building for production...
[admins]: transforming...
[admins]: ✓ 11 modules transformed.
[admins]: rendering chunks...
[admins]: computing gzip size...
[admins]: dist/index.html                  0.46 kB │ gzip:  0.29 kB
[admins]: dist/assets/index-m0DGwFy9.css   1.00 kB │ gzip:  0.54 kB
[admins]: dist/assets/index-pF6ixyOY.js   52.56 kB │ gzip: 21.23 kB
[admins]: ✓ built in 451ms
[admins]: Process exited (exit code 0), completed in 0s 835ms
[managers]: Process started
[teams]: Process started
[teams]: vite v5.0.11 building for production...
[teams]: transforming...
[teams]: ✓ 32 modules transformed.
[teams]: rendering chunks...
[teams]: computing gzip size...
[teams]: dist/index.html                   0.46 kB │ gzip:  0.30 kB
[teams]: dist/assets/index-T74ItOsL.css    0.92 kB │ gzip:  0.50 kB
[teams]: dist/assets/index-6R6vXPwk.js   143.74 kB │ gzip: 46.30 kB
[teams]: ✓ built in 565ms
[teams]: Process exited (exit code 0), completed in 0s 849ms
[managers]: vite v5.0.11 building for production...
[managers]: transforming...
[managers]: ✓ 32 modules transformed.
[managers]: rendering chunks...
[managers]: computing gzip size...
[managers]: dist/index.html                   0.46 kB │ gzip:  0.30 kB
[managers]: dist/assets/index-T74ItOsL.css    0.92 kB │ gzip:  0.50 kB
[managers]: dist/assets/index-l9QTZ6f4.js   143.74 kB │ gzip: 46.30 kB
[managers]: ✓ built in 596ms
[managers]: Process exited (exit code 0), completed in 0s 896ms
Done in 1s 735ms

The flags passed to this command are important. The --all flag runs the script in all workspaces. You can also use --since to only run the script in workspaces that have changed in the current branch compared to the main branch.

Using --from instead of --all lets you supply a glob pattern so that the script is run only in the workspaces that match the pattern. For example, the following command only runs the build script in workspaces that are in the packages directory:

yarn workspaces foreach --from packages/* -Rpt run build

The -p flag enables parallel execution, and the -t flag tells Yarn to respect the topological order. In other words, with the -t flag, Yarn runs the build script in a workspace only after all its dependencies have been built. When used together with the -p flag, you can ensure that the build process is run in parallel whenever possible while ensuring the dependencies are built first.

If you look at the output of the first yarn workspaces foreach command, you’ll notice that admins and common-ui start their build simultaneously:

[admins]: Process started
[common-ui]: Process started

This is because managers and teams depend on common-ui, which means they can’t be built until common-ui is built. However, since admins doesn’t depend on any other workspace, it can be built in parallel with common-ui.

Once common-ui finishes building, the managers and teams apps can be built. Since they don’t depend on each other, they’re also built in parallel:

[managers]: Process started
[teams]: Process started

In comparison, here the -p flag is omitted:

$ yarn workspaces foreach --all -t run build
[admins]: Process started
[admins]: vite v5.0.11 building for production...
[admins]: transforming...
[admins]: ✓ 11 modules transformed.
[admins]: rendering chunks...
[admins]: computing gzip size...
[admins]: dist/index.html                  0.46 kB │ gzip:  0.29 kB
[admins]: dist/assets/index-m0DGwFy9.css   1.00 kB │ gzip:  0.54 kB
[admins]: dist/assets/index-pF6ixyOY.js   52.56 kB │ gzip: 21.23 kB
[admins]: ✓ built in 460ms
[admins]: Process exited (exit code 0), completed in 0s 808ms

[common-ui]: Process started
[common-ui]: vite v5.0.11 building for production...
[common-ui]: transforming...
[common-ui]: ✓ 10 modules transformed.
[common-ui]: rendering chunks...
[common-ui]: computing gzip size...
[common-ui]: dist/common-ui.js  21.20 kB │ gzip: 6.32 kB
[common-ui]: dist/common-ui.umd.cjs  13.94 kB │ gzip: 5.44 kB
[common-ui]: ✓ built in 172ms
[common-ui]: Process exited (exit code 0), completed in 0s 439ms

[managers]: Process started
[managers]: vite v5.0.11 building for production...
[managers]: transforming...
[managers]: ✓ 32 modules transformed.
[managers]: rendering chunks...
[managers]: computing gzip size...
[managers]: dist/index.html                   0.46 kB │ gzip:  0.30 kB
[managers]: dist/assets/index-T74ItOsL.css    0.92 kB │ gzip:  0.50 kB
[managers]: dist/assets/index-l9QTZ6f4.js   143.74 kB │ gzip: 46.30 kB
[managers]: ✓ built in 552ms
[managers]: Process exited (exit code 0), completed in 0s 832ms

[teams]: Process started
[teams]: vite v5.0.11 building for production...
[teams]: transforming...
[teams]: ✓ 32 modules transformed.
[teams]: rendering chunks...
[teams]: computing gzip size...
[teams]: dist/index.html                   0.46 kB │ gzip:  0.30 kB
[teams]: dist/assets/index-T74ItOsL.css    0.92 kB │ gzip:  0.50 kB
[teams]: dist/assets/index-6R6vXPwk.js   143.74 kB │ gzip: 46.30 kB
[teams]: ✓ built in 578ms
[teams]: Process exited (exit code 0), completed in 0s 837ms
Done in 2s 922ms

As you can see, the workspaces are built one after another.

Creating Global Scripts

So far, you’ve learned about three different ways to run a script defined in one of the workspaces:

  1. Using yarn run <script-name> from the workspace where the script is defined
  2. Using the yarn workspace <workspace-name> run <script-name> command from the root of the project
  3. Using yarn workspaces foreach from the root of the project, provided all the workspaces define a script with the same name

However, it’s also possible to “promote” a script to a global script so that it can be run from anywhere in the project, using the typical yarn run <script-name> syntax. To create a global script, you must create a script that contains a colon (:) in its name.

Open the package.json file in the packages/common-ui directory and add the following script:

"common-ui:build": "vite build"

This script defines the same build task but is now registered as a global script. You can run it from anywhere in the project:

yarn run common-ui:build

Note: You don’t necessarily need to use the workspace name as the prefix of the global script, but it’s a good practice so that you don’t accidentally end up defining two global scripts with the same name in two different workspaces. If that happens, none of them will be promoted to global scripts.

You can find the complete project on GitHub.

Conclusion

In this article, you learned how to build a monorepo with Yarn workspaces and Vite and explored how this setup enables you to have projects with different frameworks in the same repo.

Vite is a powerful and efficient frontend build tool that makes developing JavaScript and TypeScript apps fast and easy. With Vite, you get access to a super fast development server with HMR and the freedom to use any framework of your choice.

When you outgrow yarn workspaces and vite, or need to incorporate backend languages like Go, Rust, Python or even Ruby and Java, take a look at Earthly. It’s a great build tool for monorepos and will help speed your development and build time. Check it out

Earthly Cloud: Consistent, Fast Builds, Any CI
Consistent, repeatable builds across all environments. Advanced caching for faster builds. Easy integration with any CI. 6,000 build minutes per month included.

Get Started Free

Aniket Bhattacharyea %
Aniket Bhattacharyea
Aniket is a student doing a Master's in Mathematics and has a passion for computers and software.

Published:

Get notified about new articles!
We won't send you spam. Unsubscribe at any time.