How Yarn Plug’n’Play Changes Module Resolution

Yarn’s Plug’n’Play (PnP) is an alternative dependency management strategy that completely removes the traditional node_modules folder from JavaScript projects. Instead of relying on the Node.js runtime to perform recursive file-system lookups to locate installed packages, Yarn PnP uses a single static map to resolve dependencies instantly. This approach significantly speeds up installation and startup times, eliminates redundant file copying, and strictly prevents packages from accessing undeclared dependencies.

The Traditional Node.js Module Resolution

To understand how Yarn PnP changes module resolution, it is essential to review how standard Node.js resolution works:

  1. When a file calls require('example-package'), Node.js inspects the current directory for a node_modules folder.
  2. If it is not found, it moves up one parent directory and checks for node_modules again.
  3. This process repeats recursively up to the root of the file system until the module is found or an error is thrown.
  4. Once located, Node reads the package’s package.json to identify the entry file, checks file extensions, and loads the target script.

While simple, this mechanism results in thousands of redundant file-system system calls (stat and readdir), enables “phantom dependencies” (importing packages you did not explicitly declare in your package.json), and requires huge disk space to duplicate identical packages across projects.

The Yarn Plug’n’Play Approach

Yarn Plug’n’Play abandons the node_modules folder entirely. When you run yarn install in PnP mode, Yarn performs the following steps:

  1. Global Cache Storage: Packages are downloaded once and stored as compressed .zip archives inside a global cache directory or within .yarn/cache.
  2. Generating the Manifest: Yarn generates a .pnp.cjs file at the root of the project. This file contains a complete dependency tree, mapping package names and versions to their exact locations within the cache.
  3. Injecting the Resolver: Yarn hooks directly into Node’s module resolution lifecycle (via Node’s --require or --loader flags) to override the default resolution algorithm.

How PnP Alters Module Resolution at Runtime

When an application runs under Yarn PnP, the module resolution process shifts from dynamic file-system crawling to direct in-memory lookup:

1. Static Table Lookup (\(O(1)\))

When a module executes require('lodash'), the patched resolver checks .pnp.cjs to see which package initiated the request. It looks up the caller in the manifest table, verifies that lodash is declared as a dependency for that caller, and instantly returns the exact path to the module inside the cached .zip archive without searching the disk.

2. Virtual File System Access (ZipFS)

Because the resolved paths point to zip archives, Yarn integrates a virtual file system (ZipFS). Node.js reads the files directly from the compressed archive into memory, removing the need to unpack thousands of files onto disk.

3. Strict Boundary Enforcement

Under standard resolution, any package can access any other package present in a hoisted node_modules directory, even if it is not listed in package.json. In Yarn PnP, the resolver explicitly checks permissions: if package A tries to import package B without listing package B in its package.json dependencies or peerDependencies, the resolver throws an error immediately.

Key Impacts on JavaScript Development