Yarn Modern with Plug'n'Play and "Zero-Installs"

Ever since Facebook released Yarn in 2016, it has become an essential tool for developers. According to Stack Overflow’s 2022 survey of 70,000 developers, 27% of those respondents use Yarn. Introducing a lock file and parallel downloads, Yarn pushed the envelope of what a JavaScript dependency management tool can do. The release of version 2 (known as Yarn Modern or Yarn Berry) introduced Plug’n’Play with Zero-Installs. In this article I will walk you through creating a project using the Yarn Modern, adding Plug’n’Play and Zero-Installs as we go.

What is Plug’n’Play and Zero-Installs?

Plug’n’Play is a strategy for installing a project’s dependencies. Zero-Installs is a collection of Yarn’s features used to make your project as fast and stable as possible.

Have you ran into a problem with a project’s dependencies that was only solved with rm -rf node_modules? Or running a node script on your machine works but that same script fails to run on a coworker’s? The Yarn docs calls this the “node_modules problem”.

The “node_modules problem” refers to the inefficiencies caused by using the Node Resolution Algorithm to look up a project’s dependencies. The Node Resolution Algorithm looks for files that are require’d or import’d, recursively (and indiscriminately) searching all the way to the home directory of the computer it is being run on. With the volume of files installed, and different stat and readdir calls, this traditional approach is costly in space and time. Every minute spent on CI / CD installing node modules and bootstrapping your application is money coming out of someone’s pocket.

Plug’n’Play and Zero-Installs provide a solution: replace large node_modules downloads with zipped cached files that unzip at runtime.

Setting up a new project with Yarn Modern

First, we need to upgrade to the latest version of Yarn.

npm i yarn -g

We are going to create a new folder and set up a git repo using Yarn inside of it.

mkdir yarn-modern
cd yarn-modern
git init
yarn init -y

Running ls -l should show us the following files:

.editorconfig
.gitignore
README.md
package.json
yarn.lock

We need to set up our project to install our dependencies in the node_modules folder. We can do that by running yarn config set nodeLinker: node-modules from the terminal. That shows us the following output:

➤ YN0000: Successfully set nodeLinker to 'node-modules'

Running that command creates a .yarnrc.yml file with the following contents:

nodeLinker: node-modules

We want to commit these files. Doing so will allow us to run git status to see subsequent changes in isolation. Before doing that, we need to make a change to .gitignore.

Let’s look at the contents of .gitignore first:

.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

# Swap the comments on the following lines if you don't wish to
use Zero-Installs
# Documentation here: https://yarnpkg.com/features/Zero-Installs
!.yarn/cache
#.pnp.*

There are a number of .yarn folders that we do not want to ignore. They can serve a purpose but they are beyond the scope of this article. Looking at the bottom section, there is the following comment:

# Swap the comments on the following lines if you don't wish to
# use Zero-Installs

By following those instructions we will turn off Zero-Installs, which was set up for us when we ran yarn init -y. Turning it off now will help us better understand how Zero-Installs works when we enable it.

Update the bottom of the .gitignore to this:

#!.yarn/cache
.pnp.*

Add node_modules to .gitignore as well.

When that is done, commit the changes.

Let’s add lodash as a dependency.

yarn add lodash

Running git status we should see a familiar sight:

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working
directory)
    modified:   package.json
    modified:   yarn.lock

Running ls -l, we see a node_modules folder. We also have a .yarn folder. The .yarn folder has a cache folder along with a install-state.gz folder. We won’t be covering what install-state.gz does but you can find out more about the files and folders that can end up in your .yarn folder here. .yarn/cache holds a local, cached copy of our project dependencies.

ls -ls .yarn/cache will show us something like this:

lodash-npm-4.17.21-6382451519-eb835a2e51.zip

If we were to delete node_modules and re-run yarn install, it would load it from .yarn/cache instead of fetching it from the remote repository. That is a nice touch.

Next we will create a small file in our project’s root that loads lodash, index.js.

const _ = require("lodash");

console.log(_.camelCase("FOO_BAR_BAZ"));

Running node index.js we get the following:

fooBarBaz

As expected, we are loading lodash from node_modules. Commit this change. Next we are going to enable Plug’n’Play.

Adding Plug’n’Play

To add Plug’n’Play, we need to change the nodeLinker config value. Run the following command in your terminal:

yarn config set nodeLinker pnp

Done successfully, that will output the following:

➤ YN0000: Successfully set nodeLinker to 'pnp'

You can also change this setting manually by opening .yarnrc.yml and changing nodeLinker to pnp:

nodeLinker: pnp

Run yarn install. You should see the following line somewhere in the output:

➤ YN0031: │ One or more node_modules have been detected and will
be removed. This operation may take some time.

Running ls -l should show that there is no longer a node_modules directory. Lets make sure our index.js file is still working:

node index.js

Uh oh! We got an error, that looks something like this:

node:internal/modules/cjs/loader:942
  throw err;
  ^

Error: Cannot find module 'lodash'
Require Stack:
...

Using Plug’n’Play requires using the yarn binary, like so:

yarn node index.js

It works! Running yarn node index.js, we can see console.log from our index.js file. Instead of node_modules, our dependencies are loading from .yarn/cache instead.

Zero-Installs

To achieve zero-install state with Plug’n’Play, we need make an update to .gitignore, replacing its contents with the following:

.yarn/*
!.yarn/cache
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

Running git status, you see the previously ignored Yarn cache files along with a .pnp.cjs showing up as untracked files. We will commit these files to the repo. This how we achieve Zero-Installs, by adding our .yarn/cache files to the repo along with the .pnp.cjs file. Yarn’s website has a good description of what the .pnp.cjs file is used for:

The .pnp.cjs file contains various maps: one linking package names and versions to their location on the disk and another one linking package names and versions to their list of dependencies. With these lookup tables, Yarn can instantly tell Node where to find any package it needs to access, as long as they are part of the dependency tree, and as long as this file is loaded within your environment.

Adding your dependencies to your repo may feel strange at first but you quickly get used to it.


If you are adding Plug’n’Play to a new project, you may run into some challenges. Plug’n’Play is more strict compared to other JavaScript package managers. This is by design. 3rd party packages may not have listed their dependencies in a manner that works with the strict nature of Plug’n’Play. This is either a feature or bug of how other package managers work. It all depends on how you look at it. Yarn Modern provides tools to tackle these challenges: yarn patch, yarn patch-commit, and packageDependencies. There is also some additional work needed to get your IDE to work with Plug’n’Play. I will cover these topics in a future post.

Yarn with Plug’n’Play and Zero-Installs is an exciting addition to the package manager landscape. Skipping the need to install node modules on every build results in faster deployments. Getting the latest dependencies is as simple as running git pull. While it may seem unconventional at first, once adopted, it can increase a team’s overall productivity.