Yarn 3 vs. NPM 8 for Dev Teams in 2022

Mon 24 January 2022


Me


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.
  • 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 for typescript@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 if yarn.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 of npm@7.X.X, npm ci is broken and will not fail if a module in package.json doesn't match what's in package-lock.json; see this ongoing Github thread.
    • Here is a temporary workaround.


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 the workspace-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 of node -r path/to/.pnp.cjs ./dist_folder.
  • NPM 8: Supported ✅
    • Just build your project using tsc and then node ./dist_folder.


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.