跳至主要内容

PnP API

概述

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

数据结构

PackageLocator

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

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

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

PackageInformation

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

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

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

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

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

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

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

运行时常量

process.versions.pnp

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

此值是一种便捷的方法,可用于检查您是否在即插即用环境(您可以在其中 require('pnpapi'))中运行

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

require('module')

当在 PnP API 中使用时,module 内置模块通过一个额外函数进行扩展

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

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

请注意,虽然您将能够使用返回给您的 API 解析依赖项,但您还需要确保通过使用 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 来执行相同操作

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')

在即插即用环境下运行时,一个新的内置模块将出现在您的树中,并将向所有包提供(无论它们是否在依赖项中定义):pnpapi。它公开本文档其余部分中描述的常量和函数。

请注意,我们已在 npm 注册表中保留了 pnpapi 包名,因此没有任何人能够出于邪恶目的抢夺该名称。我们稍后可能会使用它为非 PnP 环境提供一个填充(以便您无论项目是否通过 PnP 安装,都可以使用 PnP API),但到目前为止,它仍然是一个空包。

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

API 接口

VERSIONS

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

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

注意:当前版本为 3。我们负责任地增加版本,并努力使每个版本向后兼容之前的版本,但正如你可能猜到的那样,某些功能仅在最新版本中可用。

topLevel

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

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

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

注意:这些特殊的顶级定位器只是物理定位器的别名,可以通过调用 findPackageLocator 访问这些别名。

getLocator(...)

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

此函数是一个小帮手,可以更轻松地处理“引用”范围。正如您在PackageInformation接口中所看到的,packageDependencies映射值可以是字符串或元组 - 并且计算已解析定位符的方式根据该值而改变。为了避免必须手动进行Array.isArray检查,我们提供了getLocator函数,它可以为您完成此操作。

就像topLevel一样,您没有义务实际使用它 - 如果由于某种原因我们的实现不是您正在寻找的,您可以自由地推出自己的版本。

getDependencyTreeRoots(...)

export function getDependencyTreeRoots(): PackageLocator[];

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

注意:此函数将始终返回物理定位符,因此它永远不会返回topLevel部分中描述的特殊顶级定位符。

getAllLocators(...)

export function getAllLocators(): PackageLocator[];

重要:此函数不属于 Plug'n'Play 规范,仅作为 Yarn 扩展可用。为了使用它,您首先必须检查VERSIONS字典是否包含有效的getAllLocators属性。

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

getPackageInformation(...)

export function getPackageInformation(locator: PackageLocator): PackageInformation;

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

findPackageLocator(...)

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

给定磁盘上的位置,findPackageLocator函数将返回“拥有”该路径的包的包定位符。例如,对类似于/path/to/node_modules/foo/index.js的概念进行此函数将返回指向foo包(及其确切版本)的包定位符。

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

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 将返回一个不合格的解析。

例如,以下

lodash/uniq

很可能会解析为

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

正如你所见,.js 扩展名没有被添加。这是因为 合格和不合格解析 之间的差异。如果你必须获取一个准备与文件系统 API 一起使用的路径,请优先使用 resolveRequest

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

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

resolveUnqualified(...)

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

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

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

例如,以下

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

很可能会解析为

/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(...)),但更短。

就像 resolveUnqualified 一样,resolveRequest 是完全可选的,如果你已经有一个只需要添加对即插即用支持的解析管道,你可能希望跳过它直接使用更低级别的 resolveToUnqualified

例如,以下

lodash

很可能会解析为

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

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

resolveVirtual(...)

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

重要:此函数不属于即插即用规范的一部分,仅作为 Yarn 扩展提供。要使用它,您首先必须检查 VERSIONS 字典是否包含有效的 resolveVirtual 属性。

resolveVirtual 函数将接受任何路径作为参数,并返回减去任何 虚拟组件 的相同路径。只要您不在乎在此过程中丢失依赖项树信息,这将使以可移植方式存储文件位置变得更加容易(通过这些路径请求文件将阻止它们访问其对等依赖项)。

合格解析与非合格解析

本文档详细介绍了两种类型的解析:合格解析和非合格解析。尽管相似,但它们呈现出不同的特性,使其适用于不同的设置。

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

非合格解析是即插即用 API 的核心;它们表示无法通过任何其他方式获取的数据。如果您希望在解析器内集成即插即用,那么它们可能是您要找的。另一方面,如果您将 PnP API 作为一次性使用并且只想获取有关给定文件或包的一些信息,那么完全合格的解析将非常方便。

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

访问文件

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

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

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`));

遍历依赖项树

以下函数实现树遍历,以打印树中定位符的列表。

重要提示:此实现会迭代树中的所有节点,即使它们被发现多次(这种情况非常常见)。因此,执行时间远高于预期。根据需要进行优化 🙂

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);
}