Recently, while working on a personal project, I did a deep dive into Yarn 3 vs. NPM 8 for development teams. I evaluated them against a specific feature tuple that was important to me:
- native workspace support (not relying on Lerna),
- reproducible builds for the CI/CD pipeline, and
- Typescript support in VSCode.
Conclusion Up Front
I'm going to stick with NPM 8 for now. It still suffers from the npm ci
issue mentioned below but I expect that to be fixed eventually by the NPM maintainers. In the meantime I'll continue using the below workaround.
I want to like Yarn 3, and I do believe it will lead to more resilient CI/CD pipelines and more hardened builds, but the current developer experience has too much friction. I don't feel confident in using it in a team setting just yet. I'll give it one or two years and see if it improves.
The Evaluation
I reviewed a set of features, each grouped into one of three categories: design time, build time, and run time features.
Design Time Feature: Enforce package manager version
I want to ensure that each developer on the team uses the same version of NPM/Yarn when running commands against package(s) in the project. This avoids devs' environments needlessly breaking due to package manager version drift.
- Yarn 3: Supported ✅
- In your project root run:
yarn set version 3.1.0 && echo "yarnPath: .yarn/releases/yarn-3.1.0.cjs"> .yarnrc.yml
.
- In your project root run:
- NPM 8: Not Supported ❌
Design Time Feature: Go-To-Definition works out-of-the-box
I want to be able to go to the definition of a symbol by Cmd+Click ing on it. For example, in the statement: import { Express } from 'express'
being able to see the definition of Express by Cmd+Click ing on it.
- Yarn 3: Not Supported ❌
- This won't work out of the box. The following guide explains how to set it up: Editor SDKs.
- Make sure to open your VSCode project as a folder (File > Open Folder...) and not as a workspace (File > Add Folder to Workspace...) else the generated
.vscode
files won't take effect and you'll go down a four-hour Google search rabbit hole before realizing the mistake 🙃. - Finally, at the time of this writing this feature is broken for
typescript@4.5.4
and only works fortypescript@4.4.4
and down.
- NPM 8: Supported ✅
Build Time Feature: Reproducible builds
I want to guarantee that the exact module versions that were developed against are used throughout the entire CI/CD pipeline as well as in production. For example, if development was done against express@4.17.2
I don't want express@4.17.3
deployed into production even if the semver range in package.json
is ^4.17.2
. This results in more reliable builds which reduces unexpected bugs in production.
- Yarn 3: Supported ✅
yarn install --immutable
will abort ifyarn.lock
was to be modified as a result of the install.
- NPM 8: Not Supported ❌
- The command to do this is
npm ci
however as ofnpm@7.X.X
,npm ci
is broken and will not fail if a module inpackage.json
doesn't match what's inpackage-lock.json
; see this ongoing Github thread. - Here is a temporary workaround.
- The command to do this is
Build Time Feature: ignore devDependencies when building for production
I don't want to include any modules that were used in development in the production release. For example, if we used jest
for our testing it should not be part of node_modules
when deploying to production. Some benefits includes a reduced attack surface as well as making your executable more lightweight.
- Yarn 3: Supported ✅
yarn workspaces focus --production
. This requires theworkspace-tools
plugin:yarn plugin import workspace-tools
.
- NPM 8: Supported ✅
npm install --production
Run Time Feature: runtime does not require custom module loader
In an ideal world I want to execute my production server using a very simple command; something like: node ./dist_folder
. I don't want to complicate this with augmenting how Node loads modules via a custom module loader.
- Yarn 3: Not Supported ❌
- Yarn 3 uses the Plug'n'Play installation strategy. This does away with
node_modules
and instead stores, and simultaneously de-dupes, downloaded modules as zip files. This saves space, leads to faster install times, and improves node startup times. - However, this breaks some features we've come to take for granted, like Go-To-Definition (see above) and having Node take care of loading modules for us.
- Yarn 3 generates a
.pnp.cjs
file that is now responsible for loading modules from the aforementioned zip archive. You need to tell Node to use this loader before starting your application by running something to the effect ofnode -r path/to/.pnp.cjs ./dist_folder
.
- Yarn 3 uses the Plug'n'Play installation strategy. This does away with
- NPM 8: Supported ✅
- Just build your project using
tsc
and thennode ./dist_folder
.
- Just build your project using
Conclusion
I'll be sticking to NPM 8 for now. Even though I agree with the direction Yarn 3 is heading in, it took me about eight hours to get all of the above Yarn 3 features to work correctly with my project. I couldn't justify that kind of set up cost in a team setting. I'll revisit Yarn 3 in one or two years with the expectation that the developer experience has stabilized.