What is Yarn PNP and Should You Use It?

If you’re starting a new TypeScript (or JavaScript) project, you might be using a newer version of Yarn to manage your dependencies. If so, you’ll probably run into a new feature known as Yarn PNP(Plug’n’Play). This is particularly true because it’s enabled by default. So, what is it? Should you use it?

What is Yarn PNP?

Like most ambitious pieces of software, Yarn PNP aims to find a local maxima that simultaneously solves multiple discrete goals:

  • Faster installation of your dependencies, using less disk space. It’s a lot of work to unzip all of your dependencies’ packages and write them into the local `node_modules` folder. Wouldn’t it be nice to skip this step?
  • Prevent use of transitive dependencies. With a traditional flat `node_modules` folder, there’s nothing to prevent your application code from importing from any arbitrary package. For example, say your package.json lists a dependency on `foo`, and `foo` has a dependency on `bar`. Then it’s now possible for your code to also import and use `bar`. This doesn’t seem like a problem until you upgrade `foo` by a patch version. Then, it brings along a copy of `bar` that’s a major version newer than the one your application was built with. It’s obvious that this has a high risk of introducing breakage, and that you should have intentionally pinned `bar` to the version you want to use. But, it’s easy to forget. PNP can enforce this.

These are both obviously laudable goals. Yarn PNP achieves them in a relatively novel way.

How does it work?

First, Yarn will resolve your dependencies and download their .Zip files into a local cache, skipping the unzip step and the construction of the `node_modules` folder in your project.

Second, Yarn then introduces some runtime JavaScript code that patches the behavior of Node’s own CommonJS module system. It also introduces some built-in modules for things like filesystem access. These all have the goal of transparently adding the capability to use packages directly from the .Zip files that exist in the local cache.

Finally, in order to make this all work with your IDE and various tools like TypeScript, Yarn introduces some shims. The shims inject the above JavaScript code before trampolining over to the node REPL or the desired tool.

Are there any drawbacks?

Unfortunately, the monkey-patching of Node’s built-in facilities and the necessary logistics of ensuring these patches have been loaded at runtime, mean it doesn’t quite live up to its name, “Plug ‘n Play.”

The first thing to consider is that not all libraries are compatible with the illusion Yarn is presenting with PNP. Existing packages have had a long history of relying on the existence of the `node_modules` folder to accomplish a variety of things.

  • Autoloading peer dependencies. E.g., your SQL library might test for the presence of the postgres driver and automatically load/configure it if it has been installed.
  • Looking up a package’s own resources via the filesystem.
  • Naturally, these assumptions can break if PNP doesn’t present a perfect illusion. Further, some tools such as React Native’s Metro Bundler are fundamentally built around the assumption of being able to analyze your `node_modules`. Yarn’s gotten better over time, but you can’t patch every issue, and packages that you need to use may remain incompatible.

    The second thing to consider is your editor’s integration and your local environment. In order for your editor’s static analysis and language integration to work, Yarn provides “sdks” that patch required functionality. The ones for VSCode are pretty good, but this may not matter if you or your team members use a different editor.

    Even if you are using VSCode, the experience isn’t always problem-free. One example is that the ability to navigate to the source for your dependencies’ code depends on a plugin for reading Zip files. This plugin has, on at least one occasion, broken after a VSCode update.

    Verdict: Should you use it?

    When Yarn PNP works well, it’s amazing. Installation is fast, you use far less disk space, and you can’t accidentally depend on any transitive dependencies.

    Unfortunately, my conclusion is that you end up spending a non-trivial amount of your novelty budget in order to make it work. And, this novelty budget is probably best spent elsewhere.

    It’s possible that Yarn and the broader JavaScript ecosystem may overcome some of these issues. But, other manifestations of the complexity will always remain. PNP attempts to solve important goals. However, I feel these goals are ultimately best addressed at other layers in the stack, perhaps by NodeJS itself.