--- title: Managing multiples projects with Lerna and Yarn Workspaces author: Sergio Herrera authorURL: https://github.com/sergiohgz authorImageURL: https://avatars3.githubusercontent.com/u/14012309 authorTwitter: sergiohgz --- Verdaccio is a project with a big ecosystem, composed by several projects, each one with its own configurations and ways to manage. This fact transforms a simple maintenance operation, like updating a common dependency, into a real hard work. We saw a problem, these configurations entropy made harder to work with all the projects. So, we needed to simplify and unify them to make it easier. We need a **monorepo**. A monorepo is a project configuration to manage a collection of dependencies in a simple and unified way. There are many examples out there that Javascript developer use nowadays, like [Babel](https://babeljs.io/), [Create React App](https://create-react-app.dev/) or [Material UI](https://material-ui.com/). Now, we are proud to announce our [monorepo](https://github.com/verdaccio/monorepo), our big ecosystem joined in only one repository. This article is the first part of a series of articles where we will try to explain our motivation about to set up by your own, improve the management and workflows (CI, code quality, etc). ## Background and tools {#background-and-tools} A year ago, [Juan](https://twitter.com/jotadeveloper) and I met in Madrid, Spain, and were talking about the roadmap for Verdaccio 4, the scope of the projects and more. We had some concerns about the Verdaccio ecosystem we want to build, such amount of repositories requires hard work for maintenance by each one with their own dependencies, scripts, configurations, etc. We decided to unify all configurations, because handle several repositories would not be realistic and would have all things replicated in all repositories. In order to achieve our goal, and we found [Lerna](https://lerna.js.org/) and [Yarn Workspaces](https://yarnpkg.com/lang/en/docs/workspaces/). As a first step, we created the basic architecture of the monorepo and moved the first dependencies in, an ESLint config and a Babel preset. After Verdaccio 4 release, Typescript migration and more, we saw that many projects shares the same architecture, so it could be a good moment to achieve our goals. There are more tools for this purpose, but we will focus on Lerna and Yarn Workspaces. ### Lerna {#lerna} **Lerna** is a tool to manage several Javascript projects with multiple packages (called _monorepos_), optimizing the workflows around them. You can install it globally with `npm install --global lerna`, `yarn global add lerna` or your favourite package manager, to run commands with `lerna `. Also, if you don't want to install it, you can use package runners such as `npx`. ### Yarn Workspaces {#yarn-workspaces} **Yarn Workspaces** is a way to setup package architecture where all packages dependencies are installed together with a single `yarn install`. This involves two things that you could not see at the first moment. - All the packages in the workspace uses a common lockfile as a _single source of truth_. - If a package has a dependency on other package in the workspace, they are linked _without affecting your global environment_. Since Yarn 1.0, this feature is enabled by default, you only need a root `package.json` to setup them. ## Creating the monorepo {#creating-the-monorepo} The initial setup is really simple, you only need to create a new repository and run `lerna init` to initialize the monorepo. This will generate files like `package.json` or `lerna.json` (packages structure is shown as example, but not generated). ``` lerna.json package.json packages/ ├── pkg1 │ └── package.json ├── pkg2 │ └── package.json └── pkg3 └── package.json ``` Let's see the main configuration for `lerna.json` and root `package.json`. ### lerna.json {#lernajson} After initial setup, this file will looks like: ```json { "packages": ["packages/*"], "version": "0.0.0" } ``` - packages: this array defines the location for all packages that conforms the monorepo. They could be explicit (`packages/pkg1`) or, if a folder has several packages, you can use `*` wildcard. In our case, we use `core/*`, `plugins/*` and `tools/*`. - version: the version of the packages. It could be a semver value (called _fixed mode_) or `independent` if you want to let packages define their own version. Take care that _independent mode_ will create one tag for each package that will be published with its own version. We prefer _fixed mode_ to keep all packages with the same version and reduce headaches to users. Other interesting settings are: - npmClient: you can define in you want to use Yarn, Npm or your favourite client. - useWorkspaces: if you want to enable Yarn Workspaces, you will have to tell it to Lerna, setting this option to `true`. - stream: if you want to have the output of a child process inmediately in the console, you have to enable this option. Also, this will prefix each line with the package name that generate them. ### package.json {#packagejson} After initial setup, you need some configuration to allow _Lerna_ work with _Yarn Workspaces_: - workspaces: this is the array where we define the packages that are part of the workspace. In a simple way, this is the same you have defined in `lerna.json` under the _packages_ key. - private: as the root package should be a simple container, you should keep it with `true` value to not publish it. Later, you will learn how to define more settings in the root `package.json`. ## Creating and importing packages {#creating-and-importing-packages} You have a monorepo, but it's not useful at this moment. Let's create and import some packages. ### lerna create {#lerna-create} To create new packages, you can use `lerna create ` like you would do with `npm init` or `yarn init`. The wizard will ask you for some fields like package description, author or license. Other way to give that information is using command options (`--description`, `--author`, etc). Then, you have the package ready for work with it, add stuff like Babel or ESLint, dependencies and scripts to `package.json`... It's important to say that if your package is a scoped package, you have to add the next codeblock in `package.json`, but do not add it if your package is not scoped, because `lerna publish` will fail in this case: ```json "publishConfig": { "access": "public" } ``` ### lerna import {#lerna-import} If you have a project you want to import, you don't have to create a new one in the monorepo, you can import it using `lerna import `. This command will read all the Git history from the project specified and apply commit-by-commit into your monorepo to avoid losing the original history. If there were commits with conflicts, the import process will fail, but CLI propose you to use the `--flatten` option to bypass it. Also, if you want to keep original authors and committers, you can pass `--preserve-commit` option. After the import completes, you can remove unnecessary stuff like CI settings, old scripts, hoisted devDependencies... ## Managing dependencies and devDependencies {#managing-dependencies-and-devdependencies} Each package will contain their own _dependencies_ and _devDependencies_ like if the package is not in a monorepo, but there are some interesting things you can do with _devDependencies_, hoist them in the root `package.json`. Let's see with an example, _pkg1_ and _pkg2_ defines ESLint as devDependency, so you have defined that in two packages, duplicating the definition and management. You can extract it from both and set it as devDependency in the root `package.json`. This way, all the projects that need it will have it available. Now, we are going to add ESLint to _pkg3_, you only have to add its own configuration, because package has been hoisted right now. But the same doesn't work with _dependencies_, because they are needed when the package is published. A good practice is to hoist every devDependency so they will be available for every package, except in two cases: - A package needs a specific version of the package. In this case, you can have a root definition for all the packages and the specific version for the package that requires it. This will create a `node_modules` for the specific package, but not another `yarn.lock`. - Those _devDependencies_ that are part of the workspace must not be defined in the root `package.json`, because if you do that, you will create a cycle. An example is if _pkg1_ and _pkg2_ define _pkg3_ as devDependency, it could not be defined in the root because _pkg3_ will depend with itself. ## Running scripts {#running-scripts} Like _dependencies_, each package will have their own _scripts_, so you should define them in their specific `package.json`. But what happens when you want to run scripts for many packages at the same time? You don't need to extract them to the root `package.json` because they will contain specific arguments/options for each project. You can invoke scripts or commands from the root package using two _Lerna_ commands, `lerna run` and `lerna exec`. The first, `lerna run