Skip to main content

PnP API

概述

¥Overview

在 Plug'n'Play 运行时环境中运行的每个脚本都可以访问一个特殊的内置模块 (pnpapi),该模块允许你在运行时自省依赖树。

¥Every script running within a Plug'n'Play runtime environment has access to a special builtin module (pnpapi) that allows you to introspect the dependency tree at runtime.

数据结构

¥Data Structures

PackageLocator

export type PackageLocator = {
name: string,
reference: string,
};

包定位器是描述依赖树中包的一个唯一实例的对象。name 字段保证是包本身的名称,但 reference 字段应被视为不透明字符串,其值可能是 PnP 实现决定放在那里的任何值。

¥A package locator is an object describing one unique instance of a package in the dependency tree. The name field is guaranteed to be the name of the package itself, but the reference field should be considered an opaque string whose value may be whatever the PnP implementation decides to put there.

请注意,一个包定位器与其他包定位器不同:顶层定位器(可通过 pnp.topLevel 获得,参见下文)将 namereference 都设置为 null。此特殊定位器将始终镜像顶层包(通常是存储库的根,即使在使用工作区时也是如此)。

¥Note that one package locator is different from the others: the top-level locator (available through pnp.topLevel, cf below) sets both name and reference to null. This special locator will always mirror the top-level package (which is generally the root of the repository, even when working with workspaces).

PackageInformation

export type PackageInformation = {
packageLocation: string,
packageDependencies: Map<string, null | string | [string, string]>,
packagePeers: Set<string>,
linkType: 'HARD' | 'SOFT',
};

软件包信息集描述了可以在磁盘上找到软件包的位置,以及允许它需要的确切依赖集。packageDependencies 值应解释为:

¥The package information set describes the location where the package can be found on the disk, and the exact set of dependencies it is allowed to require. The packageDependencies values are meant to be interpreted as such:

  • 如果是字符串,则该值应用作定位器中的引用,其名称是依赖名称。

    ¥If a string, the value is meant to be used as a reference in a locator whose name is the dependency name.

  • 如果是 [string, string] 元组,则该值应用作定位器,其名称是元组的第一个元素,引用是第二个元素。这通常发生在包别名(例如 "foo": "npm:bar@1.2.3")中。

    ¥If a [string, string] tuple, the value is meant to be used as a locator whose name is the first element of the tuple and reference is the second one. This typically occurs with package aliases (such as "foo": "npm:bar@1.2.3").

  • 如果是 null,则指定的依赖根本不可用。这通常发生在依赖树中包的对等依赖未由其直接父级提供时。

    ¥If null, the specified dependency isn't available at all. This typically occurs when a package's peer dependency didn't get provided by its direct parent in the dependency tree.

如果存在 packagePeers 字段,则表示哪些依赖具有强制契约,即使用与依赖它们的包完全相同的实例。此字段在纯 PnP 上下文中很少有用(因为我们的实例化保证比这更严格、更可预测),但需要从 PnP 映射正确生成 node_modules 目录。

¥The packagePeers field, if present, indicates which dependencies have an enforced contract on using the exact same instance as the package that depends on them. This field is rarely useful in pure PnP context (because our instantiation guarantees are stricter and more predictable than this), but is required to properly generate a node_modules directory from a PnP map.

linkType 字段仅在特定情况下有用 - 它描述了是否要求 PnP API 的制作者通过硬链接(在这种情况下,所有 packageLocation 字段都被认为归链接器所有)或软链接(在这种情况下,packageLocation 字段代表链接器影响范围之外的位置)提供软件包。

¥The linkType field is only useful in specific cases - it describes whether the producer of the PnP API was asked to make the package available through a hard linkage (in which case all the packageLocation field is reputed being owned by the linker) or a soft linkage (in which case the packageLocation field represents a location outside of the sphere of influence of the linker).

运行时常量

¥Runtime Constants

process.versions.pnp

在 PnP 环境下操作时,此值将设置为一个数字,表示正在使用的 PnP 标准的版本(与 require('pnpapi').VERSIONS.std 完全相同)。

¥When operating under PnP environments, this value will be set to a number indicating the version of the PnP standard in use (which is strictly identical to require('pnpapi').VERSIONS.std).

此值是一种方便的方法,用于检查你是否在 Plug'n'Play 环境(你可以在其中 require('pnpapi'))下操作:

¥This value is a convenient way to check whether you're operating under a Plug'n'Play environment (where you can require('pnpapi')) or not:

if (process.versions.pnp) {
// do something with the PnP API ...
} else {
// fallback
}

require('module')

在 PnP API 中操作时,module 内置模块会扩展一个额外功能:

¥The module builtin module is extended when operating within the PnP API with one extra function:

export function findPnpApi(lookupSource: URL | string): PnpApi | null;

调用时,此函数将从给定的 lookupSource 开始遍历文件系统层次结构,以找到最近的 .pnp.cjs 文件。然后它将加载此文件,将其注册到 PnP 加载器内部存储中,并将生成的 API 返回给你。

¥When called, this function will traverse the filesystem hierarchy starting from the given lookupSource in order to locate the closest .pnp.cjs file. It'll then load this file, register it inside the PnP loader internal store, and return the resulting API to you.

请注意,虽然你可以使用返回给你的 API 来解决依赖,但你也需要使用 createRequire 确保它们已代表项目正确加载:

¥Note that while you'll be able to resolve the dependencies by using the API returned to you, you'll need to make sure they are properly loaded on behalf of the project too, by using createRequire:

const {createRequire, findPnpApi} = require(`module`);

// We'll be able to inspect the dependencies of the module passed as first argument
const targetModule = process.argv[2];

const targetPnp = findPnpApi(targetModule);
const targetRequire = createRequire(targetModule);

const resolved = targetPnp.resolveRequest(`eslint`, targetModule);
const instance = targetRequire(resolved); // <-- important! don't use `require`!

最后,可以注意到 findPnpApi 在大多数情况下实际上并不需要,而且由于其 resolve 功能,我们可以仅使用 createRequire 执行相同操作:

¥Finally, it can be noted that findPnpApi isn't actually needed in most cases and we can do the same with just createRequire thanks to its resolve function:

const {createRequire} = require(`module`);

// We'll be able to inspect the dependencies of the module passed as first argument
const targetModule = process.argv[2];

const targetRequire = createRequire(targetModule);

const resolved = targetRequire.resolve(`eslint`);
const instance = targetRequire(resolved); // <-- still important

require('pnpapi')

在 Plug'n'Play 环境下操作时,一个新的内置模块将出现在你的树中,并将提供给你的所有包(无论它们是否在其依赖中定义它):pnpapi。它公开了本文档其余部分描述的函数的常量。

¥When operating under a Plug'n'Play environment, a new builtin module will appear in your tree and will be made available to all your packages (regardless of whether they define it in their dependencies or not): pnpapi. It exposes the constants a function described in the rest of this document.

请注意,我们已在 npm 注册表中保留了 pnpapi 包名称,因此不存在任何人能够出于恶意目的抢夺该名称的风险。我们稍后可能会使用它来为非 PnP 环境提供 polyfill(这样无论项目是否通过 PnP 安装,你都可以使用 PnP API),但截至目前,它仍然是一个空包。

¥Note that we've reserved the pnpapi package name on the npm registry, so there's no risk that anyone will be able to snatch the name for nefarious purposes. We might use it later to provide a polyfill for non-PnP environments (so that you'd be able to use the PnP API regardless of whether the project got installed via PnP or not), but as of now it's still an empty package.

请注意,内置的 pnpapi 是上下文相关的:虽然来自同一依赖树的两个包保证读取同一个包,但来自不同依赖树的两个包将获得不同的实例 - 每个都反映了它们所属的依赖树。这种区别通常并不重要,除了有时对于项目生成器(通常在其自己的依赖树中运行,同时还操作它们正在生成的项目)之外。

¥Note that the pnpapi builtin is contextual: while two packages from the same dependency tree are guaranteed to read the same one, two packages from different dependency trees will get different instances - each reflecting the dependency tree they belong to. This distinction doesn't often matter except sometimes for project generator (which typically run within their own dependency tree while also manipulating the project they're generating).

API 接口

¥API Interface

VERSIONS

export const VERSIONS: {std: number, [key: string]: number};

VERSIONS 对象包含一组数字,详细说明当前公开的 API 版本。唯一保证存在的版本是 std,它将引用本文档的版本。其他键用于描述第三方实现者提供的扩展。只有当公共 API 的签名发生变化时,版本才会增加。

¥The VERSIONS object contains a set of numbers that detail which version of the API is currently exposed. The only version that is guaranteed to be there is std, which will refer to the version of this document. Other keys are meant to be used to describe extensions provided by third-party implementors. Versions will only be bumped when the signatures of the public API change.

注意

当前版本是 3。我们负责任地对其进行改进,并努力使每个版本都与之前的版本向后兼容,但你可能已经猜到有些功能仅在最新版本中可用。

¥The current version is 3. We bump it responsibly and strive to make each version backward-compatible with the previous ones, but as you can probably guess some features are only available with the latest versions.

topLevel

export const topLevel: {name: null, reference: null};

topLevel 对象是一个简单的包定位器,指向依赖树的顶层包。请注意,即使使用工作区,整个项目仍然只有一个顶层。

¥The topLevel object is a simple package locator pointing to the top-level package of the dependency tree. Note that even when using workspaces you'll still only have one single top-level for the entire project.

提供此对象是为了方便,不一定需要使用;你可以使用自己的定位器字面量创建自己的顶层定位器,并将两个字段设置为 null

¥This object is provided for convenience and doesn't necessarily needs to be used; you may create your own top-level locator by using your own locator literal with both fields set to null.

注意

这些特殊的顶层定位器仅仅是物理定位器的别名,可以通过调用 findPackageLocator 来访问。

¥These special top-level locators are merely aliases to physical locators, which can be accessed by calling findPackageLocator.

getLocator(...)

export function getLocator(name: string, referencish: string | [string, string]): PackageLocator;

此功能是一个小助手,可以更轻松地使用 "referencish" 范围。正如你可能在 PackageInformation 接口中看到的,packageDependencies 映射值可以是字符串或元组 - 并且计算解析定位器的方式会根据这一点而改变。为了避免必须手动进行 Array.isArray 检查,我们提供了 getLocator 函数来为你执行此操作。

¥This function is a small helper that makes it easier to work with "referencish" ranges. As you may have seen in the PackageInformation interface, the packageDependencies map values may be either a string or a tuple - and the way to compute the resolved locator changes depending on that. To avoid having to manually make a Array.isArray check, we provide the getLocator function that does it for you.

就像 topLevel 一样,你没有义务真正使用它 - 如果出于某种原因我们的实现不是你想要的,你可以自由推出自己的版本。

¥Just like for topLevel, you're under no obligation to actually use it - you're free to roll your own version if for some reason our implementation wasn't what you're looking for.

getDependencyTreeRoots(...)

export function getDependencyTreeRoots(): PackageLocator[];

getDependencyTreeRoots 函数将返回构成各个依赖树根的定位器集。在 Yarn 中,项目中的每个工作区只有一个这样的定位器。

¥The getDependencyTreeRoots function will return the set of locators that constitute the roots of individual dependency trees. In Yarn, there is exactly one such locator for each workspace in the project.

注意

该函数将始终返回物理定位器,因此它永远不会返回 topLevel 部分中描述的特殊顶层定位器。

¥This function will always return the physical locators, so it'll never return the special top-level locator described in the topLevel section.

getAllLocators(...)

export function getAllLocators(): PackageLocator[];
important

此功能不是 Plug'n'Play 规范的一部分,仅作为 Yarn 扩展提供。为了使用它,你首先必须检查 VERSIONS 字典是否包含有效的 getAllLocators 属性。

¥This function is not part of the Plug'n'Play specification and only available as a Yarn extension. In order to use it, you first must check that the VERSIONS dictionary contains a valid getAllLocators property.

getAllLocators 函数将返回依赖树中的所有定位器,没有特定的顺序(尽管对同一 API 的调用之间始终保持一致的顺序)。当你希望了解有关软件包本身的更多信息,但不了解有关确切的树布局时,可以使用它。

¥The getAllLocators function will return all locators from the dependency tree, in no particular order (although it'll always be a consistent order between calls for the same API). It can be used when you wish to know more about the packages themselves, but not about the exact tree layout.

getPackageInformation(...)

export function getPackageInformation(locator: PackageLocator): PackageInformation;

getPackageInformation 函数返回给定包的 PnP API 中存储的所有信息。

¥The getPackageInformation function returns all the information stored inside the PnP API for a given package.

findPackageLocator(...)

export function findPackageLocator(location: string): PackageLocator | null;

给定磁盘上的一个位置,findPackageLocator 函数将返回 "owns" 路径的包的包定位器。例如,在概念上类似于 /path/to/node_modules/foo/index.js 的东西上运行此函数将返回指向 foo 包(及其确切版本)的包定位器。

¥Given a location on the disk, the findPackageLocator function will return the package locator for the package that "owns" the path. For example, running this function on something conceptually similar to /path/to/node_modules/foo/index.js would return a package locator pointing to the foo package (and its exact version).

注意

该函数将始终返回物理定位器,因此它永远不会返回 topLevel 部分中描述的特殊顶层定位器。你可以利用此属性提取顶层包的物理定位器:

¥This function will always return the physical locators, so it'll never return the special top-level locator described in the topLevel section. You can leverage this property to extract the physical locator for the top-level package:

const virtualLocator = pnpApi.topLevel;
const physicalLocator = pnpApi.findPackageLocator(pnpApi.getPackageInformation(virtualLocator).packageLocation);

resolveToUnqualified(...)

export function resolveToUnqualified(request: string, issuer: string | null, opts?: {considerBuiltins?: boolean}): string | null;

resolveToUnqualified 函数可能是 PnP API 公开的最重要的函数。给定一个请求(可能是像 lodash 这样的裸说明符,也可能是像 ./foo.js 这样的相对/绝对路径)和触发请求的文件的路径,PnP API 将返回不合格的解析。

¥The resolveToUnqualified function is maybe the most important function exposed by the PnP API. Given a request (which may be a bare specifier like lodash, or an relative/absolute path like ./foo.js) and the path of the file that issued the request, the PnP API will return an unqualified resolution.

例如,以下内容:

¥For example, the following:

lodash/uniq

很可能被解决为:

¥Might very well be resolved into:

/my/cache/lodash/1.0.0/node_modules/lodash/uniq

如你所见,.js 扩展未添加。这是由于 合格和不合格解决方案 之间的差异。如果你必须获取可用于文件系统 API 的路径,则最好使用 resolveRequest

¥As you can see, the .js extension didn't get added. This is due to the difference between qualified and unqualified resolutions. In case you must obtain a path ready to be used with the filesystem API, prefer using resolveRequest instead.

请注意,在某些情况下,你可能只有一个文件夹作为 issuer 参数使用。发生这种情况时,只需在发行者后面加上一个额外的斜杠 (/),以向 PnP API 指示发行者是一个文件夹。

¥Note that in some cases you may just have a folder to work with as issuer parameter. When this happens, just suffix the issuer with an extra slash (/) to indicate to the PnP API that the issuer is a folder.

如果请求是内置模块,则该函数将返回 null,除非 considerBuiltins 设置为 false

¥This function will return null if the request is a builtin module, unless considerBuiltins is set to false.

resolveUnqualified(...)

export function resolveUnqualified(unqualified: string, opts?: {extensions?: string[]}): string;

resolveUnqualified 函数主要作为辅助函数提供;它重新实现了文件扩展名和文件夹索引的 Node 解析,但不实现常规的 node_modules 遍历。它使将 PnP 集成到某些项目中变得稍微容易一些,尽管如果你已经有符合要求的东西,则不需要以任何方式进行。

¥The resolveUnqualified function is mostly provided as an helper; it reimplements the Node resolution for file extensions and folder indexes, but not the regular node_modules traversal. It makes it slightly easier to integrate PnP into some projects, although it isn't required in any way if you already have something that fits the bill.

举个例子,Webpack 使用的 enhanced-resolved 不需要 resolveUnqualified,因为它已经以自己的方式实现了 resolveUnqualified(及更多)中包含的逻辑。相反,我们只需要利用更底层的 resolveToUnqualified 函数并将其提供给常规解析器。

¥To give you an example resolveUnqualified isn't needed with enhanced-resolved, used by Webpack, because it already implements its own way the logic contained in resolveUnqualified (and more). Instead, we only have to leverage the lower-level resolveToUnqualified function and feed it to the regular resolver.

例如,以下内容:

¥For example, the following:

/my/cache/lodash/1.0.0/node_modules/lodash/uniq

很可能被解决为:

¥Might very well be resolved into:

/my/cache/lodash/1.0.0/node_modules/lodash/uniq/index.js

resolveRequest(...)

export function resolveRequest(request: string, issuer: string | null, opts?: {considerBuiltins?: boolean, extensions?: string[]]}): string | null;

resolveRequest 函数是 resolveToUnqualifiedresolveUnqualified 的封装器。本质上,这有点像调用 resolveUnqualified(resolveToUnqualified(...)),但更短。

¥The resolveRequest function is a wrapper around both resolveToUnqualified and resolveUnqualified. In essence, it's a bit like calling resolveUnqualified(resolveToUnqualified(...)), but shorter.

resolveUnqualified 一样,resolveRequest 完全是可选的,如果你已经有了只需要添加对 Plug'n'Play 支持的解析管道,你可能希望跳过它而直接使用更底层的 resolveToUnqualified

¥Just like resolveUnqualified, resolveRequest is entirely optional and you might want to skip it to directly use the lower-level resolveToUnqualified if you already have a resolution pipeline that just needs to add support for Plug'n'Play.

例如,以下内容:

¥For example, the following:

lodash

很可能被解决为:

¥Might very well be resolved into:

/my/cache/lodash/1.0.0/node_modules/lodash/uniq/index.js

如果请求是内置模块,则该函数将返回 null,除非 considerBuiltins 设置为 false

¥This function will return null if the request is a builtin module, unless considerBuiltins is set to false.

resolveVirtual(...)

export function resolveVirtual(path: string): string | null;
important

此功能不是 Plug'n'Play 规范的一部分,仅作为 Yarn 扩展提供。为了使用它,你首先必须检查 VERSIONS 字典是否包含有效的 resolveVirtual 属性。

¥This function is not part of the Plug'n'Play specification and only available as a Yarn extension. In order to use it, you first must check that the VERSIONS dictionary contains a valid resolveVirtual property.

resolveVirtual 函数将接受任何路径作为参数并返回相同路径减去任何 虚拟组件。只要你不介意在此过程中丢失依赖树信息(通过这些路径请求文件将阻止它们访问其对等依赖),这样就可以更轻松地以可移植的方式将位置存储到文件中。

¥The resolveVirtual function will accept any path as parameter and return the same path minus any virtual component. This makes it easier to store the location to the files in a portable way as long as you don't care about losing the dependency tree information in the process (requiring files through those paths will prevent them from accessing their peer dependencies).

合格与不合格解决方案

¥Qualified vs Unqualified Resolutions

本文档详细介绍了两种类型的解决方案:合格和不合格。虽然相似,但它们具有不同的特性,使其适用于不同的设置。

¥This document detailed two types of resolutions: qualified and unqualified. Although similar, they present different characteristics that make them suitable in different settings.

合格和非合格解析之间的差异在于 Node 解析本身的怪癖。不合格的解析可以静态计算,而无需访问文件系统,但只能解析相对路径和裸说明符(如 lodash);它们永远不会解析文件扩展名或文件夹索引。相比之下,合格的解析已准备好用于访问文件系统。

¥The difference between qualified and unqualified resolutions lies in the quirks of the Node resolution itself. Unqualified resolutions can be statically computed without ever accessing the filesystem, but only can only resolve relative paths and bare specifiers (like lodash); they won't ever resolve the file extensions or folder indexes. By contrast, qualified resolutions are ready to be used to access the filesystem.

不合格的解析是 Plug'n'Play API 的核心;它们表示无法通过任何其他方式获取的数据。如果你希望将 Plug'n'Play 集成到解析器中,它们很可能就是你要找的东西。另一方面,如果你一次性使用 PnP API 并且只想获取有关给定文件或包的一些信息,则完全合格的解析非常方便。

¥Unqualified resolutions are the core of the Plug'n'Play API; they represent data that cannot be obtained any other way. If you're looking to integrate Plug'n'Play inside your resolver, they're likely what you're looking for. On the other hand, fully qualified resolutions are handy if you're working with the PnP API as a one-off and just want to obtain some information on a given file or package.

两种不同用例的两个绝佳选择 🙂

¥Two great options for two different use cases 🙂

访问文件

¥Accessing the files

PackageInformation 结构中返回的路径采用原生格式(因此 Linux/OSX 上为 Posix,Windows 上为 Win32),但它们可能引用典型文件系统之外的文件。对于 Yarn 来说尤其如此,它直接从其 zip 存档中引用包。

¥The paths returned in the PackageInformation structures are in the native format (so Posix on Linux/OSX and Win32 on Windows), but they may reference files outside of the typical filesystem. This is particularly true for Yarn, which references packages directly from within their zip archives.

要访问此类文件,你可以使用 @yarnpkg/fslib 项目,它在多层架构下抽象文件系统。例如,以下代码可以访问任何路径,无论它们是否存储在 zip 存档中:

¥To access such files, you can use the @yarnpkg/fslib project which abstracts the filesystem under a multi-layer architecture. For example, the following code would make it possible to access any path, regardless of whether they're stored within a zip archive or not:

const {PosixFS, ZipOpenFS} = require(`@yarnpkg/fslib`);
const libzip = require(`@yarnpkg/libzip`).getLibzipSync();

// This will transparently open zip archives
const zipOpenFs = new ZipOpenFS({libzip});

// This will convert all paths into a Posix variant, required for cross-platform compatibility
const crossFs = new PosixFS(zipOpenFs);

console.log(crossFs.readFileSync(`C:\\path\\to\\archive.zip\\package.json`));

遍历依赖树

¥Traversing the dependency tree

以下函数实现了树遍历,以便从树中打印定位器列表。

¥The following function implements a tree traversal in order to print the list of locators from the tree.

important

此实现会遍历树中的所有节点,即使它们被多次找到(这种情况很常见)。因此,执行时间比它可能的要高得多。根据需要进行优化 🙂

¥This implementation iterates over all the nodes in the tree, even if they are found multiple times (which is very often the case). As a result the execution time is way higher than it could be. Optimize as needed 🙂

const pnp = require(`pnpapi`);
const seen = new Set();

const getKey = locator =>
JSON.stringify(locator);

const isPeerDependency = (pkg, parentPkg, name) =>
getKey(pkg.packageDependencies.get(name)) === getKey(parentPkg.packageDependencies.get(name));

const traverseDependencyTree = (locator, parentPkg = null) => {
// Prevent infinite recursion when A depends on B which depends on A
const key = getKey(locator);
if (seen.has(key))
return;

const pkg = pnp.getPackageInformation(locator);
console.assert(pkg, `The package information should be available`);

seen.add(key);

console.group(locator.name);

for (const [name, referencish] of pkg.packageDependencies) {
// Unmet peer dependencies
if (referencish === null)
continue;

// Avoid iterating on peer dependencies - very expensive
if (parentPkg !== null && isPeerDependency(pkg, parentPkg, name))
continue;

const childLocator = pnp.getLocator(name, referencish);
traverseDependencyTree(childLocator, pkg);
}

console.groupEnd(locator.name);

// Important: This `delete` here causes the traversal to go over nodes even
// if they have already been traversed in another branch. If you don't need
// that, remove this line for a hefty speed increase.
seen.delete(key);
};

// Iterate on each workspace
for (const locator of pnp.getDependencyTreeRoots()) {
traverseDependencyTree(locator);
}