跳至主要內容

PnP 規範

關於此文件

為了讓第三方專案更易於互通,此文件說明我們在 Plug'n'Play 安裝策略 下安裝檔案至磁碟時遵循的規範。這也表示

  • 我們對此文件所做的任何變更都將遵循 semver 規則
  • 我們將盡力保持向後相容性
  • 新功能將旨在優雅地降級

高階概念

Plug'n'Play 的運作方式是將所有套件(依存關係樹的一部分)的表格保留在記憶體中,如此一來,我們可以輕鬆回答兩個不同的問題

  • 給定路徑,它屬於哪個套件?
  • 給定套件,它可以存取哪些依存關係?

因此,解析套件匯入變成交織這兩個操作的問題

  • 首先,找出請求解析的套件
  • 然後擷取其相依性,檢查請求的套件是否在其中
  • 如果在,則擷取相依性資訊,並傳回其位置

接著可以設計額外的功能,但這些功能是選用的。例如,Yarn 根據它對專案所了解的資訊,在無法解決相依性時拋出語意錯誤:由於我們知道整個相依性樹的狀態,我們也知道套件可能遺失的原因。

基本概念

所有套件都由定位器唯一參照。定位器是套件識別碼(如果相關,則包含其範圍)與套件參考的組合,套件參考可視為用於區分同一個套件的不同執行個體(或版本)的唯一 ID。套件參考應視為不透明值:從解析演算法的角度來看,它們以 workspace:virtual:npm: 或任何其他協定開頭並不重要。

可攜性

基於可攜性考量,清單中的所有路徑

  • 都必須使用 Unix 路徑格式(/ 作為分隔符號)。
  • 都必須相對於清單資料夾(因此它們與專案在磁碟上的位置無關)。
警告

本規範中的所有演算法都假設路徑已根據這兩個規則正規化。

備援

為了與舊有程式碼庫有更好的相容性,Plug'n'Play 支援我們稱為「備援」的功能。備援會在套件對其相依性(套件的相依性中未列出)提出解析要求時觸發。在正常情況下,解析器會拋出,但當啟用備援時,解析器應先嘗試在特定套件組的相依性中找到相依性套件。如果找到,則會透明地傳回。

在某種意義上,備援可以視為提升的一種受限且更安全的型式。雖然提升允許透過多個層級的相依性進行不受約束的存取,但備援需要明確定義備援套件(通常是頂層套件)。

套件位置

儘管 Plug'n'Play 規範本身並不要求執行時間在存取套件檔案時支援常規檔案系統以外的任何內容,但生產者可能會依賴更複雜的資料儲存機制。例如,Yarn 本身需要以下兩個我們強烈建議支援的延伸功能

Zip 存取

命名為 *.zip 的檔案必須視為資料夾,以供檔案存取之用。例如,/foo/bar.zip/package.json 需要存取位於 /foo/bar.zip zip 檔案中的 package.json 檔案。

如果撰寫 JS 工具,@yarnpkg/fslib 套件可能有所幫助,它提供了一個稱為 ZipOpenFS 的支援 zip 的檔案系統層。

虛擬資料夾

為了適當地表示列出對等相依性的套件,Yarn 依賴於一個稱為 虛擬套件 的概念。它們最顯著的特性是它們都具有不同的路徑(以便 Node.js 根據需要建立它們的執行個體),同時仍由磁碟上的同一個具體資料夾建立。

這是透過為以下配置新增路徑支援來完成的

/path/to/some/folder/__virtual__/<hash>/<n>/subpath/to/file.dat

當找到此模式時,必須移除 __virtual__/<hash>/<n> 部分,忽略 hash,並將 dirname 作業套用於 /path/to/some/folder 部分 n 次。一些範例

/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 的支援虛擬的檔案系統層。

註解

__virtual__ 資料夾名稱出現在 Yarn 3.0 中。較早的版本使用 $$virtual,但在發現此模式會觸發軟體中將路徑用作正規表示式或替換的錯誤後,我們便將其更改。例如,在 String.prototype.replace 的第二個參數中找到的 $$ 會靜默轉換為 $

清單參考

pnpEnableInlining 明確設定為 false 時,Yarn 會產生一個額外的 .pnp.data.json 檔案,其中包含下列欄位。

此文件僅涵蓋資料檔案本身 - 您應該定義自己的記憶體中資料結構,在執行階段使用清單中的資訊填入。例如,Yarn 會將 packageRegistryData 表格轉換成兩個獨立的記憶體表格:一個將路徑對應到套件,另一個將套件對應到路徑。

資訊

您可能會注意到,各個地方都使用陣列元組來取代對應。這主要是為了讓 ES6 對應更容易補充,但有時也會使用非字串鍵(例如,在特定情況下,packageRegistryData 會有一個 null 鍵)。

Plug'n'Play 資料檔案包含專案中使用的套件及其依賴項。

__資訊

任意字串的陣列;僅用作標頭欄位,為 Yarn 使用者提供一些背景資訊。
"此檔案會自動產生。請勿變更,否則,
"您的修改可能會遺失。",
],

dependencyTreeRoots

依賴項樹根的套件定位器清單。通常專案中每個工作區會有一個條目(至少一個,因為頂層套件本身就是一個工作區)。
name: "@app/monorepo",
reference: "workspace:.",
}, {
name: "@app/website",
reference: "workspace:website",
}],

ignorePatternData

一個可為 null 的正規表示式。如果設定,所有相對於專案的匯入者路徑都應該與它相符。如果相符,解析應該遵循傳統的 Node.js 解析演算法,而不是 Plug'n'Play 演算法。請注意,與清單中的其他路徑不同,與此正規表示式相符的路徑不會以 `./` 開頭。
ignorePatternData: "^examples(/|$)",

enableTopLevelFallback

如果為 true,如果未在 `fallbackExclusionList` 中明確列出匯入者的依賴解析失敗,執行時期必須先檢查解析是否會對 `fallbackPool` 中的任何套件成功;如果會,則透明地傳回此解析。請注意,頂層套件的所有依賴關係都隱含地屬於 fallback 池,即使在此未列出。

fallbackPool

一個定位元配置圖,所有套件都可以存取,無論它們是否在自己的依賴關係中列出它們。
"@app/monorepo",
"workspace:.",
]],

fallbackExclusionList

一個套件配置圖,即使啟用,也絕不能使用 fallback 邏輯。金鑰是套件識別碼,值是參考集合。將識別碼與每個個別參考結合,即可產生受影響的定位元集合。
"@app/server",
["workspace:sources/server"],
]],

packageRegistryData

這是 PnP 資料檔的主要部分。此表格包含所有套件的清單,首先按套件識別碼鍵入,然後按套件參考鍵入。一個條目在兩個欄位中都會有 `null`,並代表絕對頂層套件。
[null, [
[null, {

packageRegistryData.packageLocation

封裝在磁碟上的位置,相對於 Plug'n'Play 清單。此路徑必須以 `./` 或 `../` 開頭,且必須以 `/` 結尾。

packageRegistryData.packageDependencies

封裝被允許存取的依賴項集合。每個項目都是一個元組,其中第一個鍵是封裝名稱,而值是封裝參考。請注意,此參考可能是 null!這只會在缺少對等依賴項時發生。
["react", "npm:18.0.0"],
],

packageRegistryData.linkType

可以是 SOFT 或 HARD。硬封裝連結是最常見的,表示目標位置完全由封裝管理員擁有。另一方面,軟連結通常指向磁碟上使用者定義的任意位置。
對於大多數實作人員來說,連結類型不應很重要 - 它只因為將 Plug'n'Play 樹轉換為 node_modules 樹時涉及一些細微差別而需要。
linkType: "SOFT" | "HARD",

packageRegistryData.discardFromLookup

如果為 true,此選用欄位表示當 Plug'n'Play 執行時期嘗試找出包含特定路徑的封裝時,不應考慮該封裝。例如,這是我們在使用 `link:` 協定時使用的,因為它們通常指向封裝的子資料夾,而不是其他封裝。

packageRegistryData.packagePeers

對等相依套件清單。如同 `linkType`,此欄位並未由 Plug'n'Play 執行階段本身使用,而僅由可能想利用資料檔建立 node_modules 資料夾的工具使用。
}],
]],
["react", [
["npm:18.0.0", {

packageRegistryData.packageLocation

封裝在磁碟上的位置,相對於 Plug'n'Play 清單。此路徑必須以 `./` 或 `../` 開頭,且必須以 `/` 結尾。
packageLocation: "./.yarn/cache/react-npm-18.0.0-a0b1c2d3.zip",

packageRegistryData.packageDependencies

封裝被允許存取的依賴項集合。每個項目都是一個元組,其中第一個鍵是封裝名稱,而值是封裝參考。請注意,此參考可能是 null!這只會在缺少對等依賴項時發生。
["react-dom", null],
],

packageRegistryData.linkType

可以是 SOFT 或 HARD。硬封裝連結是最常見的,表示目標位置完全由封裝管理員擁有。另一方面,軟連結通常指向磁碟上使用者定義的任意位置。
對於大多數實作人員來說,連結類型不應很重要 - 它只因為將 Plug'n'Play 樹轉換為 node_modules 樹時涉及一些細微差別而需要。
linkType: "SOFT" | "HARD",

packageRegistryData.discardFromLookup

如果為 true,此選用欄位表示當 Plug'n'Play 執行時期嘗試找出包含特定路徑的封裝時,不應考慮該封裝。例如,這是我們在使用 `link:` 協定時使用的,因為它們通常指向封裝的子資料夾,而不是其他封裝。

packageRegistryData.packagePeers

對等相依套件清單。如同 `linkType`,此欄位並未由 Plug'n'Play 執行階段本身使用,而僅由可能想利用資料檔建立 node_modules 資料夾的工具使用。
"react-dom",
],
}],
]],
],

解析演算法

資訊

為求簡潔,此演算法並未提及所有允許將模組對應至另一個模組的 Node.js 功能,例如 importsexports 或其他供應商特定功能。

NM_RESOLVE

NM_RESOLVE(specifier, parentURL)
  1. 此函式在 Node.js 文件 中有說明

PNP_RESOLVE

PNP_RESOLVE(specifier, parentURL)
  1. resolved未定義

  2. 如果 specifier 是 Node.js 內建,則

    1. resolved 設為 specifier 本身,並傳回
  3. 否則,如果 specifier 是絕對路徑或以 "./" 或 "../" 為字首的路徑,則

    1. resolved 設定為 NM_RESOLVE(specifier, parentURL) 並傳回
  4. 否則,

    1. 註:specifier 現在是一個裸識別碼

    2. unqualified 成為 RESOLVE_TO_UNQUALIFIED(specifier, parentURL)

    3. resolved 設定為 NM_RESOLVE(unqualified, parentURL)

RESOLVE_TO_UNQUALIFIED

RESOLVE_TO_UNQUALIFIED(specifier, parentURL)
  1. resolved未定義

  2. identmodulePath 成為 PARSE_BARE_IDENTIFIER(specifier) 的結果

  3. manifest 成為 FIND_PNP_MANIFEST(parentURL)

  4. 如果 manifest 為空值,則

    1. resolved 設定為 NM_RESOLVE(specifier, parentURL) 並傳回
  5. parentLocator 成為 FIND_LOCATOR(manifest, parentURL)

  6. 如果 parentLocator 為空值,則

    1. resolved 設定為 NM_RESOLVE(specifier, parentURL) 並傳回
  7. parentPkg 成為 GET_PACKAGE(manifest, parentLocator)

  8. referenceOrAlias 成為 parentPkg.packageDependencies 中由 ident 參照的項目

  9. 如果 referenceOrAliasnullundefined,則

    1. 如果 manifest.enableTopLevelFallbacktrue,則

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

        1. fallback 成為 RESOLVE_VIA_FALLBACK(manifest, ident)

        2. 如果 fallback 不是 null 也不是 undefined

          1. referenceOrAlias 設定為 fallback
  10. 如果 referenceOrAlias 仍然是 undefined,則

    1. 擲出解析錯誤
  11. 如果 referenceOrAlias 仍然是 null,則

    1. 註:這表示 parentPkgident 有未滿足的同儕相依性

    2. 擲出解析錯誤

  12. 否則,如果 referenceOrAlias 是陣列,則

    1. alias 成為 referenceOrAlias

    2. dependencyPkg 成為 GET_PACKAGE(manifest, alias)

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

  13. 否則,

    1. reference 成為 referenceOrAlias

    2. dependencyPkg 成為 GET_PACKAGE(manifest, {ident, reference})

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

GET_PACKAGE

GET_PACKAGE(manifest, locator)
  1. referenceMap 成為 parentPkg.packageRegistryData 中由 locator.ident 參照的項目

  2. pkg 成為 referenceMap 中由 locator.reference 參照的項目

  3. 傳回 pkg

    1. 注意:pkg 在此處不能為 undefined;任何 Plug'n'Play 資料表中所引用的所有套件 必須packageRegistryData 內有對應的項目。

FIND_LOCATOR

FIND_LOCATOR(manifest, moduleUrl)

注意:此處所描述的演算法相當低效。在讀取清單時,您應確保準備好更適合此任務的資料結構。

  1. bestLength0

  2. bestLocatornull

  3. relativeUrlmanifestmoduleUrl 之間的相對路徑

    1. 注意:相對路徑不得以 ./ 開頭;若有需要,請將其移除
  4. 如果 relativeUrl 符合 manifest.ignorePatternData,則

    1. 傳回 null
  5. relativeUrlWithDotrelativeUrl,並視需要加上 ./../ 前綴

  6. 對於 manifest.packageRegistryData 中的每個 referenceMap

    1. 對於 referenceMap 中的每個 registryPkg

      1. 如果 registryPkg.discardFromLookup 不為 true,則

        1. 如果 registryPkg.packageLocation.length 大於 bestLength,則

          1. 如果 relativeUrlregistryPkg.packageLocation 開頭,則

            1. bestLength 設為 registryPkg.packageLocation.length

            2. bestLocator 設為目前的 registryPkg 定位器

  7. 傳回 bestLocator

RESOLVE_VIA_FALLBACK

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

  2. referenceOrAliastopLevelPkg.packageDependencies 中由 ident 參照的項目

  3. 如果 referenceOrAlias 已定義,則

    1. 立即傳回
  4. 否則,

    1. referenceOrAliasmanifest.fallbackPool 中由 ident 參照的項目

    2. 立即傳回,無論其是否已定義

FIND_PNP_MANIFEST

FIND_PNP_MANIFEST(url)

尋找要使用於解析的正確 PnP 清單並不總是容易的。有兩個主要的選項

  • 假設有一個涵蓋整個專案的單一 PnP 清單。這是最常見的情況,即使在參照第三方專案(例如透過 portal: 協定)時,其相依性樹狀結構也會儲存在與主專案相同的清單中。

    為執行此操作,請在流程開始時呼叫 FIND_CLOSEST_PNP_MANIFEST(require.main.filename) 一次,快取其結果,並在每次呼叫 FIND_PNP_MANIFEST 時傳回(如果您在 Node.js 中執行,甚至可以使用 require.resolve('pnpapi'),它會為您執行這項工作)。

  • 嘗試在多專案世界中運作。這很少需要。我們在 Node.js PnP 載入器內支援它,但僅是因為透過 yarn create react-app 執行的「專案產生器」工具,例如 create-react-app,需要兩個不同的專案(產生器專案產生的專案)在同一個 Node.js 程序中合作。

    支援此使用案例很困難,因為它需要一個簿記機制來追蹤用於存取模組的清單,盡可能重複使用它們,並僅在鏈斷裂時尋找新的清單。

FIND_CLOSEST_PNP_MANIFEST

FIND_CLOSEST_PNP_MANIFEST(url)
  1. manifestnull

  2. directoryPathurl 的目錄

  3. pnpPathdirectoryPath/.pnp.cjs 連接

  4. 如果 pnpPath 存在於檔案系統中,則

    1. pnpDataPathdirectoryPath/.pnp.data.json 連接

    2. manifest 設定為 JSON.parse(readFile(pnpDataPath))

    3. manifest.dirPath 設定為 directoryPath

    4. 傳回 manifest

  5. 否則,如果 directoryPath/,則

    1. 傳回 null
  6. 否則,

    1. 傳回 FIND_PNP_MANIFEST(directoryPath)

PARSE_BARE_IDENTIFIER

PARSE_BARE_IDENTIFIER(specifier)
  1. 如果 specifier 以「@」開頭,則

    1. 如果 specifier 不包含「/」分隔符號,則

      1. 擲回錯誤
    2. 否則,

      1. ident 設定為 specifier 的子字串,直到第二個「/」分隔符號或字串結尾,以先發生的為準
  2. 否則,

    1. ident 設定為 specifier 的子字串,直到第一個「/」分隔符號或字串結尾,以先發生的為準
  3. modulePath 設定為從 ident.length 開始的 specifier 子字串

  4. 傳回 {ident, modulePath}