This content originally appeared on Bits and Pieces - Medium and was authored by Jonathan Saring
Dependency management in a Javascript monorepo is made much simpler with pnpm and Bit.
This post is merely an intorduction to this awesome post by the creator of pnpm and core Bit developer Zoltan Kochan. All credit goes to him.
Dependency management in javascript is the number 1 pain for developers according to the 2021 “State of JS” survey. That’s not much of a surprise. Not far behind it in 2nd place, comes code architecture.
When you try to effectively manage dependencies in a monorepo you end up facing these two problems together at the same time. You’re basically up against a dependency Megazord. That can be a painful experience.
Monorepos can provide a better developer experience in some cases and advantages in terms of code-sharing in order to, in some cases, make development easier. Many companies from Goole and Uber to startups leverage a monorepo in certain cases to gain these benefits.
However, choosing a monorepo architecture comes with a price. It leads to many issues, especially at scale, that can make it increasingly hard to develop and maintain the software projects inside it.
Let’s take a close look at one of the most painful problems on monorepos: Managing dependencies. We’ll show how using a combination of open-source tools such as Pnpm or Yarn workspaces and Bit, this problem can be elegantly solved and make it much easier to work with dependencies in a monorepo.
Pains of managing dependencies in a monorepo
So why is dependency management in a monorepo painful even as you use a Yarn or Pnpm workspace?
We would expect that once the monorepo has only one dependency management system during development things will become much simpler. There is just one set of dependencies that every package in the monorepo has access to. But that’s a double edges sword. What works in development might not work in production and things can easily break. Let’s see how.
Phantom dependencies break the code when you publish
With Pnpm or yarn workspace every package in the monorepo has its own package.json and it dependencies are defined in it. Yet all dependencies in the workspace are in the root of the monorepo.
At first, this looks like a huge power boost: Every project or package in the monorepo as access to use every dependency library.
But what happens when a package you develop in the monorepo imports and uses a depnedncay it has access to, but that isn’t defined in its package.json ?Then your tests will pass during development but will fail when you publish.
There are two ways to solve this; You can use a linter to alert you in real time that you are using a dependency’s code that’s not in your package.json or you can use a feature like pnpm’s isolated directory structure:
workspace
├── node_modules
│ └── .pnpm
│ ├── lodash@4.17.21/node_modules/lodash
│ └── ramda@0.28.0/node_modules/ramda
├── button
│ └── node_modules
│ └── lodash --> ../../node_modules/.pnpm/lodash@4.17.0/node_modules/lodash
└── card
└── node_modules
└── ramda --> ../../node_modules/.pnpm/ramda@0.28.0/node_modules/ramda
A similar problem arises with dev dependencies. Your project will run nicely and pass tests locally but when installed as a dependency it will break.
Dependency versions
Another huge pain is working with dependency versions in the monorepo.
The problem is that you might have a dependency (like Lodash or prettier) installed in the root, and a different versions in the package.json of the package you’re currently developing inside the monorepo.
The result is that your VSCode will “see” one version while when you publish the package it will use another version of the same dependency. The chance of breaking, in this case, is very high.
Solutions like manually checking versions are very time-costly and hard to do because the project keeps changing and so does every package in it. Automatic solutions and libraries can help but usually, come with a price as they blindly determine a single version regardless of consequences.
Solution: Yarn or Pnpm workspaces with Bit
Bit is an open-source project built for developing in components.
The Bit workspace add a virtual layer on top of your codebase that lets you develop and compose “components” which in this case are packages.
Bit solves many of the hard things about developing many packages in the same codebase, including the versioning and installation of dependencies — both those coming from inside and outside your projects.
When Bit is combined with pnpm, you get the most advanced and simple developer experience for working in a monorepo. In fact, Bit uses pnpm for installation under the hood and both mechanisms are developed by the same person in the open-source community.
How it works
Basically, once you develop in a Bit workspace you no longer care about the dependencies of the “component” (i.e. package) you work on. You also don’t care if they are dev or runtime dependencies or which version they’re on.
Let’s say you work in a Bit workspace and wants to add a new package in your monorepo called “new-lib”. You can create it as a component like this:
bit init
bit create node new-lib
Next you edit the code file and add an import statement to use Ramda:
import R from 'ramda';
export function lib() {
return 'Hello world!';
}
Now your code uses Ramda but you don’t have it as a dependency yet.
If you use bit-status you learn that you’re missing Ramda, and if relevant also React in case you want to render “compositions” (visual examples) of the component. Let’s say you want to add both.
Just run:
bit install react ramda
Bit’s workspace “knows” which component is missing these dependencies and will add it accordingly. You don’t need to specifically define it. Furthermore, Bit will “know” if it’s a dev or runtime dependency and add it accordingly.
Radical, isn’t it?
Then run bit show new-lib and Bit will tell you which dependencies it has and which are dev or other dependencies. Here’s an example (where new-lib is named just ‘lib’):
Dependencies no longer used in your code
What happens when you stop using a dependency in your code? Removing the import statement? Then Bit “knows” it’s no longer used in your code and it will remove it without you having to manually do it (like with pnpm/yarn workspaces without Bit).
When you remove an import statement you don’t always know if this dependency is used in another file, so being a responsible developer, you just leave it there. With Bit added that’s no longer a problem.
The only thing you’ll need to do manually is to decide if to remove a dependency from the workspace completely. At the package level, Bit abstracts away this work completely.
Test dependencies
When a dependency is used in the tests of a package but not in the code, and it’s installed in the workspace, without Bit you would need to install it as a dependency of the package. With Bit, as long as the dependency is defined in the workspace, you don’t need to. Run bit show new-lib After adding the dependency to the tests and see that its defined for the component.
Dependency version
Unlike in a traditional workspace, in a Bit workspace you list all your dependencies in a single place, in the workspace.jsonc file. After you run bit install ramda, this is how the dependencies section in your workspace.jsonc will look like:
"teambit.dependencies/dependency-resolver": {
"packageManager": "teambit.dependencies/pnpm",
"policy": {
"dependencies": {
"ramda": "0.28.0"
}
}
},
Sometimes, you might need to have a different version of ramda in some of the packages. In this case, in a traditional workspace you would use other versions of ramda in some of the package.json files. In case of Bit, you may use variants to configure groups of components. For instance, if you need to use ramda@0.27" for any package that is inside the ui` directory, use this configuration:
"teambit.dependencies/dependency-resolver": {
"packageManager": "teambit.dependencies/pnpm",
"policy": {
"dependencies": {
"ramda": "0.28.0"
}
}
},
"teambit.workspace/variants": {
"{ui/**}": {
"teambit.dependencies/dependency-resolver": {
"policy": {
"dependencies": {
"ramda": "0.27.0"
}
}
}
}
},
How is that possible?
Bit isn’t a package manager. It plays with pnpm and actually uses it.
A Bit workspace has no package.json for components (packages) in it. Insted, it dynamically generates the package.json files and passes them directly to the package manager, using its programmatical API. This is handled by the dependency resolver aspect. pnpm is used as the package manager and you can change to Yarn in the workspace.jsonc file.
Watch live demo:
Read full blog post:
Painless monorepo dependency management with Bit
How to Easily Manage Dependencies in a JS Monorepo was originally published in Bits and Pieces on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Bits and Pieces - Medium and was authored by Jonathan Saring
Jonathan Saring | Sciencx (2022-06-22T10:10:18+00:00) How to Easily Manage Dependencies in a JS Monorepo. Retrieved from https://www.scien.cx/2022/06/22/how-to-easily-manage-dependencies-in-a-js-monorepo/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.