Skip to main content

PnP 规范

关于本文档

¥About this document

为了使第三方项目的互操作性更容易,本文档描述了我们在 Plug'n'Play 安装策略 下将文件安装到磁盘上时遵循的规范。这也意味着:

¥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:

  • 我们对本文档所做的任何更改都将遵循 semver 规则

    ¥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 的工作原理是在内存中保存依赖树中所有包部分的表,这样我们就可以轻松回答两个不同的问题:

¥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

然后可以设计额外的功能,但这些功能是可选的。例如,当无法解析依赖时,Yarn 利用其了解的有关项目的信息抛出语义错误:由于我们知道整个依赖树的状态,我们也知道包可能丢失的原因。

¥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

所有包都由定位器唯一引用。定位器是包标识(如果相关,包括其范围)和包引用的组合,包引用可视为用于区分同一包的不同实例(或版本)的唯一 ID。包引用应被视为不透明值:从解析算法的角度来看,它们以 workspace:virtual:npm: 或任何其他协议开头并不重要。

¥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:

  • 必须使用 unix 路径格式(/ 作为分隔符)。

    ¥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).

警告

本规范中的所有算法都假定路径已根据这两个规则进行了规范化。

¥All algorithms in this specification assume that paths have been normalized according to these two rules.

后备

¥Fallback

为了提高与旧代码库的兼容性,Plug'n'Play 支持我们称为 "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

虽然 Plug'n'Play 规范本身并不要求运行时在访问包文件时支持除常规文件系统之外的任何其他东西,但生产者可能依赖更复杂的数据存储机制。例如,Yarn 本身需要以下两个扩展,我们强烈建议支持它们:

¥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 访问

¥Zip access

为了访问文件,必须将名为 *.zip 的文件视为文件夹。例如,/foo/bar.zip/package.json 需要访问位于 /foo/bar.zip zip 存档中的 package.json 文件。

¥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.

如果编写 JS 工具,@yarnpkg/fslib 包可能会有所帮助,提供一个名为 ZipOpenFS 的 zip 感知文件系统层。

¥If writing a JS tool, the @yarnpkg/fslib package may be of assistance, providing a zip-aware filesystem layer called ZipOpenFS.

虚拟文件夹

¥Virtual folders

为了正确表示列出对等依赖的包,Yarn 依赖于一个名为 虚拟包 的概念。它们最显着的特性是它们都有不同的路径(以便 Node.js 可以根据需要多次实例化它们),同时仍然由磁盘上的同一个具体文件夹烘焙。

¥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

当发现此模式时,必须删除 __virtual__/<hash>/<n> 部分,忽略 hash,并将 dirname 操作应用于 /path/to/some/folder 部分 n 次。一些示例:

¥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

如果编写 JS 工具,@yarnpkg/fslib 包可能会有所帮助,提供一个名为 VirtualFS 的虚拟感知文件系统层。

¥If writing a JS tool, the @yarnpkg/fslib package may be of assistance, providing a virtual-aware filesystem layer called VirtualFS.

注意

__virtual__ 文件夹名称出现在 Yarn 3.0 中。早期版本使用 $$virtual,但我们在发现这种模式会触发软件中的错误后对其进行了更改,其中路径被用作正则表达式或替换。例如,在 String.prototype.replace 的第二个参数中找到的 $$ 默默地变成了 $

¥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 software where paths were used as either regexps or replacement. For example, $$ found in the second parameter from String.prototype.replace silently turned into $.

清单参考

¥Manifest reference

pnpEnableInlining 明确设置为 false 时,Yarn 将生成一个包含以下字段的附加 .pnp.data.json 文件。

¥When pnpEnableInlining is explicitly set to false, Yarn will generate an additional .pnp.data.json file containing the following fields.

本文档仅涵盖数据文件本身 - 你应该定义自己的内存数据结构,并在运行时使用清单中的信息进行填充。例如,Yarn 将 packageRegistryData 表变成两个单独的内存表:一个将路径映射到包,另一个将包映射到路径。

¥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.

信息

你可能会注意到各个地方都使用元组数组代替映射。这主要是为了更容易地补充 ES6 映射,但有时也是为了拥有非字符串键(例如,在特定情况下 packageRegistryData 将具有 null 键)。

¥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 a null key in one particular case).

即插即用数据文件包含项目中使用的软件包集及其依赖。

__info

任意字符串数组;仅用作标头字段,为 Yarn 用户提供一些上下文。
"This file is automatically generated. Do not touch it, or risk",
"your modifications being lost.",
],

dependencyTreeRoots

依赖树根的包定位器列表。项目中的每个工作区通常都有一个条目(始终至少有一个,因为顶层包本身就是一个工作区)。
name: "@app/monorepo",
reference: "workspace:.",
}, {
name: "@app/website",
reference: "workspace:website",
}],

ignorePatternData

可空的正则表达式。如果设置,则所有相对于项目的导入器路径都应与其匹配。如果匹配成功,则解析应遵循经典的 Node.js 解析算法,而不是即插即用算法。请注意,与清单中的其他路径不同,针对此正则表达式进行检查的路径不会以 `./` 开头。
ignorePatternData: "^examples(/|$)",

enableTopLevelFallback

如果为 true,如果未在 `fallbackExclusionList` 中明确列出的导入器的依赖解析失败,则运行时必须首先检查该解析是否会成功解析 `fallbackPool` 中的任何包;如果会,则透明地返回此解析。请注意,来自顶层包的所有依赖都隐式属于后备池,即使未在此处列出。

fallbackPool

所有包都可以访问的定位器映射,无论它们是否在其依赖中列出它们。
"@app/monorepo",
"workspace:.",
]],

fallbackExclusionList

即使启用,也绝不能使用回退逻辑的包映射。键是包标识,值是引用集。将标识与每个单独的引用组合起来,可得到一组受影响的定位器。
"@app/server",
["workspace:sources/server"],
]],

packageRegistryData

这是 PnP 数据文件的主要部分。该表包含所有包的列表,首先按包标识键入,然后按包引用键入。一个条目将在两个字段中都有 `null`,并代表绝对顶层包。
[null, [
[null, {

packageRegistryData.packageLocation

相对于即插即用清单,包在磁盘上的位置。此路径必须以 `./` 或 `../` 开头,并且必须以尾随的 `/` 结尾。

packageRegistryData.packageDependencies

允许包访问的依赖集。每个条目都是一个元组,其中第一个键是包名称,值是包引用。请注意,此引用可能为空!仅当缺少对等依赖时才会发生这种情况。
["react", "npm:18.0.0"],
],

packageRegistryData.linkType

可以是 SOFT 或 HARD。硬包链接是最常见的,意味着目标位置完全由包管理器拥有。另一方面,软链接通常指向磁盘上任意的用户定义位置。
链接类型对于大多数实现者来说并不重要 - 它只是因为将即插即用树转换为 node_modules 树涉及一些微妙之处才需要它。
linkType: "SOFT" | "HARD",

packageRegistryData.discardFromLookup

如果为 true,则此可选字段表示当即插即用运行时尝试找出包含给定路径的包时,不必考虑该包。例如,这就是我们在使用 `link:` 协议时使用的内容,因为它们通常指向包的子文件夹,而不是其他包。

packageRegistryData.packagePeers

对等依赖的包列表。与 `linkType` 一样,此字段不由 Plug'n'Play 运行时本身使用,而仅由可能希望利用数据文件创建 node_modules 文件夹的工具使用。
}],
]],
["react", [
["npm:18.0.0", {

packageRegistryData.packageLocation

相对于即插即用清单,包在磁盘上的位置。此路径必须以 `./` 或 `../` 开头,并且必须以尾随的 `/` 结尾。
packageLocation: "./.yarn/cache/react-npm-18.0.0-a0b1c2d3.zip",

packageRegistryData.packageDependencies

允许包访问的依赖集。每个条目都是一个元组,其中第一个键是包名称,值是包引用。请注意,此引用可能为空!仅当缺少对等依赖时才会发生这种情况。
["react-dom", null],
],

packageRegistryData.linkType

可以是 SOFT 或 HARD。硬包链接是最常见的,意味着目标位置完全由包管理器拥有。另一方面,软链接通常指向磁盘上任意的用户定义位置。
链接类型对于大多数实现者来说并不重要 - 它只是因为将即插即用树转换为 node_modules 树涉及一些微妙之处才需要它。
linkType: "SOFT" | "HARD",

packageRegistryData.discardFromLookup

如果为 true,则此可选字段表示当即插即用运行时尝试找出包含给定路径的包时,不必考虑该包。例如,这就是我们在使用 `link:` 协议时使用的内容,因为它们通常指向包的子文件夹,而不是其他包。

packageRegistryData.packagePeers

对等依赖的包列表。与 `linkType` 一样,此字段不由 Plug'n'Play 运行时本身使用,而仅由可能希望利用数据文件创建 node_modules 文件夹的工具使用。
"react-dom",
],
}],
]],
],

解析算法

¥Resolution algorithm

信息

为简单起见,此算法未提及允许将一个模块映射到另一个模块的所有 Node.js 功能,例如 importsexports 或其他特定于供应商的功能。

¥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

NM_RESOLVE(specifier, parentURL)
  1. 该函数在 Node.js 文档 中指定

    ¥This function is specified in the Node.js documentation

PNP_RESOLVE

PNP_RESOLVE(specifier, parentURL)
  1. resolved 为 undefined

    ¥Let resolved be undefined

  2. 如果 specifier 是 Node.js 内置函数,则

    ¥If specifier is a Node.js builtin, then

    1. resolved 设置为 specifier 本身并返回它

      ¥Set resolved to specifier itself and return it

  3. 否则,如果 specifier 是绝对路径或以 "./" 或 "../" 为前缀的路径,则

    ¥Otherwise, if specifier is either an absolute path or a path prefixed with "./" or "../", then

    1. resolved 设置为 NM_RESOLVE(specifier, parentURL) 并返回它

      ¥Set resolved to NM_RESOLVE(specifier, parentURL) and return it

  4. 否则,

    ¥Otherwise,

    1. 注意:specifier 现在是一个裸标识符

      ¥Note: specifier is now a bare identifier

    2. unqualifiedRESOLVE_TO_UNQUALIFIED(specifier, parentURL)

      ¥Let unqualified be RESOLVE_TO_UNQUALIFIED(specifier, parentURL)

    3. resolved 设置为 NM_RESOLVE(unqualified, parentURL)

      ¥Set resolved to NM_RESOLVE(unqualified, parentURL)

RESOLVE_TO_UNQUALIFIED

RESOLVE_TO_UNQUALIFIED(specifier, parentURL)
  1. resolved 为 undefined

    ¥Let resolved be undefined

  2. identmodulePath 成为 PARSE_BARE_IDENTIFIER(specifier) 的结果

    ¥Let ident and modulePath be the result of PARSE_BARE_IDENTIFIER(specifier)

  3. manifestFIND_PNP_MANIFEST(parentURL)

    ¥Let manifest be FIND_PNP_MANIFEST(parentURL)

  4. 如果 manifest 为空,则

    ¥If manifest is null, then

    1. resolved 设置为 NM_RESOLVE(specifier, parentURL) 并返回它

      ¥Set resolved to NM_RESOLVE(specifier, parentURL) and return it

  5. parentLocatorFIND_LOCATOR(manifest, parentURL)

    ¥Let parentLocator be FIND_LOCATOR(manifest, parentURL)

  6. 如果 parentLocator 为空,则

    ¥If parentLocator is null, then

    1. resolved 设置为 NM_RESOLVE(specifier, parentURL) 并返回它

      ¥Set resolved to NM_RESOLVE(specifier, parentURL) and return it

  7. parentPkgGET_PACKAGE(manifest, parentLocator)

    ¥Let parentPkg be GET_PACKAGE(manifest, parentLocator)

  8. referenceOrAlias 成为 ident 引用的来自 parentPkg.packageDependencies 的条目

    ¥Let referenceOrAlias be the entry from parentPkg.packageDependencies referenced by ident

  9. 如果 referenceOrAlias 为空或未定义,则

    ¥If referenceOrAlias is null or undefined, then

    1. 如果 manifest.enableTopLevelFallback 为真,则

      ¥If manifest.enableTopLevelFallback is true, then

      1. 如果 parentLocator 不在 manifest.fallbackExclusionList 中,则

        ¥If parentLocator isn't in manifest.fallbackExclusionList, then

        1. fallbackRESOLVE_VIA_FALLBACK(manifest, ident)

          ¥Let fallback be RESOLVE_VIA_FALLBACK(manifest, ident)

        2. 如果 fallback 既不为空也不为未定义

          ¥If fallback is neither null nor undefined

          1. referenceOrAlias 设置为 fallback

            ¥Set referenceOrAlias to fallback

  10. 如果 referenceOrAlias 仍未定义,则

    ¥If referenceOrAlias is still undefined, then

    1. 抛出解析错误

      ¥Throw a resolution error

  11. 如果 referenceOrAlias 仍为空,则

    ¥If referenceOrAlias is still null, then

    1. 注意:这意味着 parentPkgident 具有未实现的对等依赖

      ¥Note: It means that parentPkg has an unfulfilled peer dependency on ident

    2. 抛出解析错误

      ¥Throw a resolution error

  12. 否则,如果 referenceOrAlias 是数组,则

    ¥Otherwise, if referenceOrAlias is an array, then

    1. aliasreferenceOrAlias

      ¥Let alias be referenceOrAlias

    2. dependencyPkgGET_PACKAGE(manifest, alias)

      ¥Let dependencyPkg be GET_PACKAGE(manifest, alias)

    3. 返回 path.resolve(manifest.dirPath, dependencyPkg.packageLocation, modulePath)

      ¥Return path.resolve(manifest.dirPath, dependencyPkg.packageLocation, modulePath)

  13. 否则,

    ¥Otherwise,

    1. referencereferenceOrAlias

      ¥Let reference be referenceOrAlias

    2. dependencyPkgGET_PACKAGE(manifest, {ident, reference})

      ¥Let dependencyPkg be GET_PACKAGE(manifest, {ident, reference})

    3. 返回 path.resolve(manifest.dirPath, dependencyPkg.packageLocation, modulePath)

      ¥Return path.resolve(manifest.dirPath, dependencyPkg.packageLocation, modulePath)

GET_PACKAGE

GET_PACKAGE(manifest, locator)
  1. referenceMap 成为 locator.ident 引用的来自 parentPkg.packageRegistryData 的条目

    ¥Let referenceMap be the entry from parentPkg.packageRegistryData referenced by locator.ident

  2. pkg 成为 locator.reference 引用的来自 referenceMap 的条目

    ¥Let pkg be the entry from referenceMap referenced by locator.reference

  3. 返回 pkg

    ¥Return pkg

    1. 注意:pkg 不能在此处未定义;任何 Plug'n'Play 数据表 MUST 中引用的所有包在 packageRegistryData 内都有相应的条目。

      ¥Note: pkg cannot be undefined here; all packages referenced in any of the Plug'n'Play data tables MUST have a corresponding entry inside packageRegistryData.

FIND_LOCATOR

FIND_LOCATOR(manifest, moduleUrl)
注意

此处描述的算法效率很低。阅读清单时,应确保准备更适合此任务的数据结构。

¥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.

  1. bestLength 为 0

    ¥Let bestLength be 0

  2. bestLocator 为 null

    ¥Let bestLocator be null

  3. relativeUrl 成为 manifestmoduleUrl 之间的相对路径

    ¥Let relativeUrl be the relative path between manifest and moduleUrl

    1. 注意:相对路径不能以 ./ 开头;如果需要,请修剪它

      ¥Note: The relative path must not start with ./; trim it if needed

  4. 如果 relativeUrlmanifest.ignorePatternData 匹配,则

    ¥If relativeUrl matches manifest.ignorePatternData, then

    1. 返回 null

      ¥Return null

  5. relativeUrlWithDot 成为 relativeUrl,并根据需要添加 ./../ 作为前缀

    ¥Let relativeUrlWithDot be relativeUrl prefixed with ./ or ../ as necessary

  6. 对于 manifest.packageRegistryData 中的每个 referenceMap

    ¥For each referenceMap value in manifest.packageRegistryData

    1. 对于 referenceMap 中的每个 registryPkg

      ¥For each registryPkg value in referenceMap

      1. 如果 registryPkg.discardFromLookup 不为真,则

        ¥If registryPkg.discardFromLookup isn't true, then

        1. 如果 registryPkg.packageLocation.length 大于 bestLength,则

          ¥If registryPkg.packageLocation.length is greater than bestLength, then

          1. 如果 relativeUrlregistryPkg.packageLocation 开头,然后

            ¥If relativeUrl starts with registryPkg.packageLocation, then

            1. bestLength 设置为 registryPkg.packageLocation.length

              ¥Set bestLength to registryPkg.packageLocation.length

            2. bestLocator 设置为当前 registryPkg 定位器

              ¥Set bestLocator to the current registryPkg locator

  7. 返回 bestLocator

    ¥Return bestLocator

RESOLVE_VIA_FALLBACK

RESOLVE_VIA_FALLBACK(manifest, ident)
  1. topLevelPkgGET_PACKAGE(manifest, {null, null})

    ¥Let topLevelPkg be GET_PACKAGE(manifest, {null, null})

  2. referenceOrAlias 成为 ident 引用的来自 topLevelPkg.packageDependencies 的条目

    ¥Let referenceOrAlias be the entry from topLevelPkg.packageDependencies referenced by ident

  3. 如果 referenceOrAlias 已定义,则

    ¥If referenceOrAlias is defined, then

    1. 立即返回

      ¥Return it immediately

  4. 否则,

    ¥Otherwise,

    1. referenceOrAlias 成为 ident 引用的来自 manifest.fallbackPool 的条目

      ¥Let referenceOrAlias be the entry from manifest.fallbackPool referenced by ident

    2. 无论是否定义,都立即返回

      ¥Return it immediately, whether it's defined or not

FIND_PNP_MANIFEST

FIND_PNP_MANIFEST(url)

找到用于解析的正确 PnP 清单并不总是那么容易。主要有两个选项:

¥Finding the right PnP manifest to use for a resolution isn't always trivial. There are two main options:

  • 假设有一个 PnP 清单涵盖整个项目。这是最常见的情况,因为即使引用第三方项目(例如通过 portal: 协议),它们的依赖树也存储在与主项目相同的清单中。

    ¥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.

    为此,请在进程开始时调用一次 FIND_CLOSEST_PNP_MANIFEST(require.main.filename),缓存其结果,并在每次调用 FIND_PNP_MANIFEST 时返回它(如果你在 Node.js 中运行,你甚至可以使用 require.resolve('pnpapi') 为你完成这项工作)。

    ¥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 use require.resolve('pnpapi') which will do this work for you).

  • 尝试在多项目世界中操作。这很少需要。我们在 Node.js PnP 加载器中支持它,但仅仅是因为 "项目生成器" 工具(如 create-react-app)通过 yarn create react-app 运行,并且需要两个不同的项目(生成器 and 生成的项目)在同一个 Node.js 进程中协作。

    ¥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 via yarn create react-app and require two different projects (the generator one and 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

FIND_CLOSEST_PNP_MANIFEST(url)
  1. manifest 为 null

    ¥Let manifest be null

  2. directoryPath 成为 url 的目录

    ¥Let directoryPath be the directory for url

  3. pnpPath 成为 directoryPath/.pnp.cjs 连接

    ¥Let pnpPath be directoryPath concatenated with /.pnp.cjs

  4. 如果 pnpPath 存在于文件系统中,则

    ¥If pnpPath exists on the filesystem, then

    1. pnpDataPath 成为 directoryPath/.pnp.data.json 连接

      ¥Let pnpDataPath be directoryPath concatenated with /.pnp.data.json

    2. manifest 设置为 JSON.parse(readFile(pnpDataPath))

      ¥Set manifest to JSON.parse(readFile(pnpDataPath))

    3. manifest.dirPath 设置为 directoryPath

      ¥Set manifest.dirPath to directoryPath

    4. 返回 manifest

      ¥Return manifest

  5. 否则,如果 directoryPath/,则

    ¥Otherwise, if directoryPath is /, then

    1. 返回 null

      ¥Return null

  6. 否则,

    ¥Otherwise,

    1. 返回 FIND_PNP_MANIFEST(directoryPath)

      ¥Return FIND_PNP_MANIFEST(directoryPath)

PARSE_BARE_IDENTIFIER

PARSE_BARE_IDENTIFIER(specifier)
  1. 如果 specifier 以 "@" 开头,然后

    ¥If specifier starts with "@", then

    1. 如果 specifier 不包含 "/" 分隔符,则

      ¥If specifier doesn't contain a "/" separator, then

      1. 抛出错误

        ¥Throw an error

    2. 否则,

      ¥Otherwise,

      1. ident 设置为 specifier 的子字符串,直到第二个 "/" 分隔符或字符串的末尾,以先发生者为准

        ¥Set ident to the substring of specifier until the second "/" separator or the end of string, whatever happens first

  2. 否则,

    ¥Otherwise,

    1. ident 设置为 specifier 的子字符串,直到第一个 "/" 分隔符或字符串的末尾,以先发生者为准

      ¥Set ident to the substring of specifier until the first "/" separator or the end of string, whatever happens first

  3. modulePath 设置为从 ident.length 开始的 specifier 的子字符串

    ¥Set modulePath to the substring of specifier starting from ident.length

  4. 返回 {ident, modulePath}

    ¥Return {ident, modulePath}