PnP API
概述
在 Plug'n'Play 运行时环境中运行的每个脚本都可以访问一个特殊的内置模块 (pnpapi
),它允许你在运行时内省依赖项树。
数据结构
PackageLocator
export type PackageLocator = {
name: string,
reference: string,
};
包定位器是一个对象,描述依赖项树中包的一个唯一实例。
字段保证是包本身的名称,但 name
reference
字段应被视为一个不透明的字符串,其值可能是 PnP 实现决定放在那里的任何内容。
请注意,一个包定位器与其他定位器不同:顶级定位器(可通过 pnp.topLevel
获得,见下文)将
和 name
reference
都设置为 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
函数是 resolveToUnqualified
和 resolveUnqualified
的包装器。从本质上讲,它有点像调用 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);
}