About this document
Section titled “About this document”To make interoperability easier for third-party projects, this document describes the specification we follow when installing files on disk under the Plug’n’Play install strategy. It also means:
- any change we make to this document will follow semver rules
- we’ll do our best to preserve backward compatibility
- new features will be intended to gracefully degrade
High-level idea
Section titled “High-level idea”Plug’n’Play works by keeping in memory a table of all packages part of the dependency tree, in such a way that we can easily answer two different questions:
- Given a path, what package does it belong to?
- Given a package, where are the dependencies it can access?
Resolving a package import thus becomes a matter of interlacing those two operations:
- First, locate which package is requesting the resolution
- Then retrieve its dependencies, check if the requested package is amongst them
- If it is, then retrieve the dependency information, and return its location
Extra features can then be designed, but are optional. For example, Yarn leverages the information it knows about the project to throw semantic errors when a dependency cannot be resolved: since we know the state of the whole dependency tree, we also know why a package may be missing.
Basic concepts
Section titled “Basic concepts”All packages are uniquely referenced by locators. A locator is a combination of a package ident, which includes its scope if relevant, and a package reference, which can be seen as a unique ID used to distinguish different instances (or versions) of a same package. The package references should be treated as an opaque value: it doesn’t matter from a resolution algorithm perspective that they start with workspace:, virtual:, npm:, or any other protocol.
Portability
Section titled “Portability”For portability reasons, all paths inside of the manifests:
- must use the unix path format (
/as separators). - must be relative to the manifest folder (so they are the same regardless of the location of the project on disk).
Fallback
Section titled “Fallback”For improved compatibility with legacy codebases, Plug’n’Play supports a feature we call “fallback”. The fallback triggers when a package makes a resolution request to a dependency it doesn’t list in its dependencies. In normal circumstances the resolver would throw, but when the fallback is enabled the resolver should first try to find the dependency packages amongst the dependencies of a set of special packages. If it finds it, it then returns it transparently.
In a sense, the fallback can be seen as a limited and safer form of hoisting. While hoisting allows unconstrainted access through multiple levels of dependencies, the fallback requires to explicitly define a fallback package - usually the top-level one.
Package locations
Section titled “Package locations”While the Plug’n’Play specification doesn’t by itself require runtimes to support anything else than the regular filesystem when accessing package files, producers may rely on more complex data storage mechanisms. For instance, Yarn itself requires the two following extensions which we strongly recommend to support:
Zip access
Section titled “Zip access”Files named *.zip must be treated as folders for the purpose of file access. For instance, /foo/bar.zip/package.json requires to access the package.json file located within the /foo/bar.zip zip archive.
If writing a JS tool, the @yarnpkg/fslib package may be of assistance, providing a zip-aware filesystem layer called ZipOpenFS.
Virtual folders
Section titled “Virtual folders”In order to properly represent packages listing peer dependencies, Yarn relies on a concept called Virtual Packages. Their most notable property is that they all have different paths (so that Node.js instantiates them as many times as needed), while still being baked by the same concrete folder on disk.
This is done by adding path support for the following scheme:
/path/to/some/folder/__virtual__/<hash>/<n>/subpath/to/file.datWhen this pattern is found, the __virtual__/<hash>/<n> part must be removed, the hash ignored, and the dirname operation applied n times to the /path/to/some/folder part. Some examples:
/path/to/some/folder/__virtual__/a0b1c2d3/0/subpath/to/file.dat/path/to/some/folder/subpath/to/file.dat
/path/to/some/folder/__virtual__/e4f5a0b1/0/subpath/to/file.dat/path/to/some/folder/subpath/to/file.dat (different hash, same result)
/path/to/some/folder/__virtual__/a0b1c2d3/1/subpath/to/file.dat/path/to/some/subpath/to/file.dat
/path/to/some/folder/__virtual__/a0b1c2d3/3/subpath/to/file.dat/path/subpath/to/file.datIf writing a JS tool, the @yarnpkg/fslib package may be of assistance, providing a virtual-aware filesystem layer called VirtualFS.
Manifest reference
Section titled “Manifest reference”When pnpEnableInlining is explicitly set to false, Yarn will generate an additional .pnp.data.json file containing the following fields.
This document only covers the data file itself - you should define your own in-memory data structures, populated at runtime with the information from the manifest. For example, Yarn turns the packageRegistryData table into two separate memory tables: one that maps a path to a package, and another that maps a package to a path.
Resolution algorithm
Section titled “Resolution algorithm”NM_RESOLVE
Section titled “NM_RESOLVE”NM_RESOLVE(specifier, parentURL)- This function is specified in the Node.js documentation
PNP_RESOLVE
Section titled “PNP_RESOLVE”PNP_RESOLVE(specifier, parentURL)-
Let
resolvedbe undefined -
If
specifieris a Node.js builtin, then- Set
resolvedtospecifieritself and return it
- Set
-
Otherwise, if
specifieris either an absolute path or a path prefixed with ”./” or ”../”, then- Set
resolvedtoNM_RESOLVE(specifier, parentURL)and return it
- Set
-
Otherwise,
-
Note:
specifieris now a bare identifier -
Let
unqualifiedbeRESOLVE_TO_UNQUALIFIED(specifier, parentURL) -
Set
resolvedtoNM_RESOLVE(unqualified, parentURL)
-
RESOLVE_TO_UNQUALIFIED
Section titled “RESOLVE_TO_UNQUALIFIED”RESOLVE_TO_UNQUALIFIED(specifier, parentURL)-
Let
resolvedbe undefined -
Let
identandmodulePathbe the result ofPARSE_BARE_IDENTIFIER(specifier) -
Let
manifestbeFIND_PNP_MANIFEST(parentURL) -
If
manifestis null, then- Set
resolvedtoNM_RESOLVE(specifier, parentURL)and return it
- Set
-
Let
parentLocatorbeFIND_LOCATOR(manifest, parentURL) -
If
parentLocatoris null, then- Set
resolvedtoNM_RESOLVE(specifier, parentURL)and return it
- Set
-
Let
parentPkgbeGET_PACKAGE(manifest, parentLocator) -
Let
referenceOrAliasbe the entry fromparentPkg.packageDependenciesreferenced byident -
If
referenceOrAliasis null or undefined, then-
If
manifest.enableTopLevelFallbackis true, then-
If
parentLocatorisn’t inmanifest.fallbackExclusionList, then-
Let
fallbackbeRESOLVE_VIA_FALLBACK(manifest, ident) -
If
fallbackis neither null nor undefined- Set
referenceOrAliastofallback
- Set
-
-
-
-
If
referenceOrAliasis still undefined, then- Throw a resolution error
-
If
referenceOrAliasis still null, then-
Note: It means that
parentPkghas an unfulfilled peer dependency onident -
Throw a resolution error
-
-
Otherwise, if
referenceOrAliasis an array, then-
Let
aliasbereferenceOrAlias -
Let
dependencyPkgbeGET_PACKAGE(manifest, alias) -
Return
path.resolve(manifest.dirPath, dependencyPkg.packageLocation, modulePath)
-
-
Otherwise,
-
Let
referencebereferenceOrAlias -
Let
dependencyPkgbeGET_PACKAGE(manifest, {ident, reference}) -
Return
path.resolve(manifest.dirPath, dependencyPkg.packageLocation, modulePath)
-
GET_PACKAGE
Section titled “GET_PACKAGE”GET_PACKAGE(manifest, locator)-
Let
referenceMapbe the entry fromparentPkg.packageRegistryDatareferenced bylocator.ident -
Let
pkgbe the entry fromreferenceMapreferenced bylocator.reference -
Return
pkg- Note:
pkgcannot be undefined here; all packages referenced in any of the Plug’n’Play data tables MUST have a corresponding entry insidepackageRegistryData.
- Note:
FIND_LOCATOR
Section titled “FIND_LOCATOR”FIND_LOCATOR(manifest, moduleUrl)-
Let
bestLengthbe 0 -
Let
bestLocatorbe null -
Let
relativeUrlbe the relative path betweenmanifestandmoduleUrl- Note: The relative path must not start with
./; trim it if needed
- Note: The relative path must not start with
-
If
relativeUrlmatchesmanifest.ignorePatternData, then- Return null
-
Let
relativeUrlWithDotberelativeUrlprefixed with./or../as necessary -
For each
referenceMapvalue inmanifest.packageRegistryData-
For each
registryPkgvalue inreferenceMap-
If
registryPkg.discardFromLookupisn’t true, then-
If
registryPkg.packageLocation.lengthis greater thanbestLength, then-
If
relativeUrlstarts withregistryPkg.packageLocation, then-
Set
bestLengthtoregistryPkg.packageLocation.length -
Set
bestLocatorto the currentregistryPkglocator
-
-
-
-
-
-
Return
bestLocator
RESOLVE_VIA_FALLBACK
Section titled “RESOLVE_VIA_FALLBACK”RESOLVE_VIA_FALLBACK(manifest, ident)-
Let
topLevelPkgbeGET_PACKAGE(manifest, {null, null}) -
Let
referenceOrAliasbe the entry fromtopLevelPkg.packageDependenciesreferenced byident -
If
referenceOrAliasis defined, then- Return it immediately
-
Otherwise,
-
Let
referenceOrAliasbe the entry frommanifest.fallbackPoolreferenced byident -
Return it immediately, whether it’s defined or not
-
FIND_PNP_MANIFEST
Section titled “FIND_PNP_MANIFEST”FIND_PNP_MANIFEST(url)Finding the right PnP manifest to use for a resolution isn’t always trivial. There are two main options:
-
Assume that there is a single PnP manifest covering the whole project. This is the most common case, as even when referencing third-party projects (for example via the
portal:protocol) their dependency trees are stored in the same manifest as the main project.To do that, call
FIND_CLOSEST_PNP_MANIFEST(require.main.filename)once at the start of the process, cache its result, and return it for each call toFIND_PNP_MANIFEST(if you’re running in Node.js, you can even userequire.resolve('pnpapi')which will do this work for you). -
Try to operate within a multi-project world. This is rarely required. We support it inside the Node.js PnP loader, but only because of “project generator” tools like
create-react-appwhich are run viayarn create react-appand require two different projects (the generator oneandthe generated one) to cooperate within the same Node.js process.Supporting this use case is difficult, as it requires a bookkeeping mechanism to track the manifests used to access modules, reusing them as much as possible and only looking for a new one when the chain breaks.
FIND_CLOSEST_PNP_MANIFEST
Section titled “FIND_CLOSEST_PNP_MANIFEST”FIND_CLOSEST_PNP_MANIFEST(url)-
Let
manifestbe null -
Let
directoryPathbe the directory forurl -
Let
pnpPathbedirectoryPathconcatenated with/.pnp.cjs -
If
pnpPathexists on the filesystem, then-
Let
pnpDataPathbedirectoryPathconcatenated with/.pnp.data.json -
Set
manifesttoJSON.parse(readFile(pnpDataPath)) -
Set
manifest.dirPathtodirectoryPath -
Return
manifest
-
-
Otherwise, if
directoryPathis/, then- Return null
-
Otherwise,
- Return
FIND_PNP_MANIFEST(directoryPath)
- Return
PARSE_BARE_IDENTIFIER
Section titled “PARSE_BARE_IDENTIFIER”PARSE_BARE_IDENTIFIER(specifier)-
If
specifierstarts with ”@”, then-
If
specifierdoesn’t contain a ”/” separator, then- Throw an error
-
Otherwise,
- Set
identto the substring ofspecifieruntil the second ”/” separator or the end of string, whatever happens first
- Set
-
-
Otherwise,
- Set
identto the substring ofspecifieruntil the first ”/” separator or the end of string, whatever happens first
- Set
-
Set
modulePathto the substring ofspecifierstarting fromident.length -
Return
{ident, modulePath}