PnP Specification
Edit this page on GitHubAbout 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
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
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
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 that they can be the same regardless of the location of the project on disk).
Important: This specification assumes all paths to have been normalized to the unix path format (
/
as separators).
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
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
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
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.dat
When 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.dat
If writing a JS tool, the @yarnpkg/fslib
package may be of assistance, providing a virtual-aware filesystem layer called VirtualFS
.
Note: The
__virtual__
folder name appeared with Yarn 3.0. Earlier releases used$$virtual
, but we changed it after discovering that this pattern triggered bugs in softwares where paths were used as either regexps or replacement. For example,$$
found in the second parameter fromString.prototype.replace
silently turned into$
.
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.
Note: You may notice that various places use arrays of tuples in place of maps. This is mostly intended to make it easier to hydrate ES6 maps, but also sometimes to have non-string keys (for instance
packageRegistryData
will have anull
key in one particular case).
__info
dependencyTreeRoots
ignorePatternData
enableTopLevelFallback
fallbackPool
fallbackExclusionList
packageRegistryData
packageRegistryData.packageLocation
packageRegistryData.packageDependencies
packageRegistryData.linkType
packageRegistryData.discardFromLookup
packageRegistryData.packagePeers
packageRegistryData.packageLocation
packageRegistryData.packageDependencies
packageRegistryData.linkType
packageRegistryData.discardFromLookup
packageRegistryData.packagePeers
Resolution algorithm
Note: for simplicity, this algorithm doesn't mention all the Node.js features that allow mapping a module to another, such as
imports
,exports
, or other vendor-specific features.
NM_RESOLVE(specifier
, parentURL
)
- This function is specified in the Node.js documentation
PNP_RESOLVE(specifier
, parentURL
)
Let
resolved
be undefinedIf
specifier
is a Node.js builtin, then- Set
resolved
tospecifier
itself and return it
- Set
Otherwise, if
specifier
is either an absolute path or a path prefixed with "./" or "../", then- Set
resolved
to NM_RESOLVE(specifier
,parentURL
) and return it
- Set
Otherwise,
Note:
specifier
is now a bare identifierLet
unqualified
be RESOLVE_TO_UNQUALIFIED(specifier
,parentURL
)Set
resolved
to NM_RESOLVE(unqualified
,parentURL
)
RESOLVE_TO_UNQUALIFIED(specifier
, parentURL
)
Let
resolved
be undefinedLet
ident
andmodulePath
be the result of PARSE_BARE_IDENTIFIER(specifier
)Let
manifest
be FIND_PNP_MANIFEST(parentURL
)If
manifest
is null, then- Set
resolved
to NM_RESOLVE(specifier
,parentURL
) and return it
- Set
Let
parentLocator
be FIND_LOCATOR(manifest
,parentURL
)If
parentLocator
is null, then- Set
resolved
to NM_RESOLVE(specifier
,parentURL
) and return it
- Set
Let
parentPkg
be GET_PACKAGE(manifest
,parentLocator
)Let
referenceOrAlias
be the entry fromparentPkg.packageDependencies
referenced byident
If
referenceOrAlias
is null or undefined, thenIf
manifest.enableTopLevelFallback
is true, thenIf
parentLocator
isn't inmanifest.fallbackExclusionList
, thenLet
fallback
be RESOLVE_VIA_FALLBACK(manifest
,ident
)If
fallback
is neither null nor undefined- Set
referenceOrAlias
tofallback
- Set
If
referenceOrAlias
is still undefined, then- Throw a resolution error
If
referenceOrAlias
is still null, thenNote: It means that
parentPkg
has an unfulfilled peer dependency onident
Throw a resolution error
Otherwise, if
referenceOrAlias
is an array, thenLet
alias
bereferenceOrAlias
Let
dependencyPkg
be GET_PACKAGE(manifest
,alias
)Return
path.resolve(manifest.dirPath, dependencyPkg.packageLocation, modulePath)
Otherwise,
Let
reference
bereferenceOrAlias
Let
dependencyPkg
be GET_PACKAGE(manifest
, {ident
,reference
})Return
path.resolve(manifest.dirPath, dependencyPkg.packageLocation, modulePath)
GET_PACKAGE(manifest
, locator
)
Let
referenceMap
be the entry fromparentPkg.packageRegistryData
referenced bylocator.ident
Let
pkg
be the entry fromreferenceMap
referenced bylocator.reference
Return
pkg
- Note:
pkg
cannot be undefined here; all packages referenced in any of the Plug'n'Play data tables MUST have a corresponding entry insidepackageRegistryData
.
- Note:
FIND_LOCATOR(manifest
, moduleUrl
)
Note: The algorithm described here is quite inefficient. You should make sure to prepare data structure more suited for this task when you read the manifest.
Let
bestLength
be 0Let
bestLocator
be nullLet
relativeUrl
be the relative path betweenmanifest
andmoduleUrl
- Note: The relative path must not start with
./
; trim it if needed
- Note: The relative path must not start with
If
relativeUrl
matchesmanifest.ignorePatternData
, then- Return null
Let
relativeUrlWithDot
berelativeUrl
prefixed with./
or../
as necessaryFor each
referenceMap
value inmanifest.packageRegistryData
For each
registryPkg
value inreferenceMap
If
registryPkg.discardFromLookup
isn't true, thenIf
registryPkg.packageLocation.length
is greater thanbestLength
, thenIf
relativeUrl
starts withregistryPkg.packageLocation
, thenSet
bestLength
toregistryPkg.packageLocation.length
Set
bestLocator
to the currentregistryPkg
locator
Return
bestLocator
RESOLVE_VIA_FALLBACK(manifest
, ident
)
Let
topLevelPkg
be GET_PACKAGE(manifest
, {null, null})Let
referenceOrAlias
be the entry fromtopLevelPkg.packageDependencies
referenced byident
If
referenceOrAlias
is defined, then- Return it immediately
Otherwise,
Let
referenceOrAlias
be the entry frommanifest.fallbackPool
referenced byident
Return it immediatly, whether it's defined or not
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 to FIND_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-app
which are run viayarn create react-app
and require two different projects (the generator oneand
the 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(url
)
Let
manifest
be nullLet
directoryPath
be the directory forurl
Let
pnpPath
bedirectoryPath
concatenated with/.pnp.cjs
If
pnpPath
exists on the filesystem, thenLet
pnpDataPath
bedirectoryPath
concatenated with/.pnp.data.json
Set
manifest
toJSON.parse(readFile(pnpDataPath))
Set
manifest.dirPath
todirectoryPath
Return
manifest
Otherwise, if
directoryPath
is/
, then- Return null
Otherwise,
- Return FIND_PNP_MANIFEST(
directoryPath
)
- Return FIND_PNP_MANIFEST(
PARSE_BARE_IDENTIFIER(specifier
)
If
specifier
starts with "@", thenIf
specifier
doesn't contain a "/" separator, then- Throw an error
Otherwise,
- Set
ident
to the substring ofspecifier
until the second "/" separator or the end of string, whatever happens first
- Set
Otherwise,
- Set
ident
to the substring ofspecifier
until the first "/" separator or the end of string, whatever happens first
- Set
Set
modulePath
to the substring ofspecifier
starting fromident.length
Return {
ident
,modulePath
}