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
鍵)。
__資訊
dependencyTreeRoots
ignorePatternData
enableTopLevelFallback
fallbackPool
fallbackExclusionList
packageRegistryData
packageRegistryData.packageLocation
packageRegistryData.packageDependencies
packageRegistryData.linkType
packageRegistryData.discardFromLookup
packageRegistryData.packagePeers
packageRegistryData.packageLocation
packageRegistryData.packageDependencies
packageRegistryData.linkType
packageRegistryData.discardFromLookup
packageRegistryData.packagePeers
解析演算法
NM_RESOLVE
NM_RESOLVE(specifier, parentURL)
- 此函式在 Node.js 文件 中有說明
PNP_RESOLVE
PNP_RESOLVE(specifier, parentURL)
-
讓
resolved
為 未定義 -
如果
specifier
是 Node.js 內建,則- 將
resolved
設為specifier
本身,並傳回
- 將
-
否則,如果
specifier
是絕對路徑或以 "./" 或 "../" 為字首的路徑,則- 將
resolved
設定為NM_RESOLVE
(specifier, parentURL)
並傳回
- 將
-
否則,
-
註:
specifier
現在是一個裸識別碼 -
讓
unqualified
成為RESOLVE_TO_UNQUALIFIED
(specifier, parentURL)
-
將
resolved
設定為NM_RESOLVE
(unqualified, parentURL)
-
RESOLVE_TO_UNQUALIFIED
RESOLVE_TO_UNQUALIFIED(specifier, parentURL)
-
讓
resolved
為 未定義 -
讓
ident
和modulePath
成為PARSE_BARE_IDENTIFIER
(specifier)
的結果 -
讓
manifest
成為FIND_PNP_MANIFEST
(parentURL)
-
如果
manifest
為空值,則- 將
resolved
設定為NM_RESOLVE
(specifier, parentURL)
並傳回
- 將
-
讓
parentLocator
成為FIND_LOCATOR
(manifest, parentURL)
-
如果
parentLocator
為空值,則- 將
resolved
設定為NM_RESOLVE
(specifier, parentURL)
並傳回
- 將
-
讓
parentPkg
成為GET_PACKAGE
(manifest, parentLocator)
-
讓
referenceOrAlias
成為parentPkg.packageDependencies
中由ident
參照的項目 -
如果
referenceOrAlias
為 null 或 undefined,則-
如果
manifest.enableTopLevelFallback
為 true,則-
如果
parentLocator
不在manifest.fallbackExclusionList
中,則-
讓
fallback
成為RESOLVE_VIA_FALLBACK
(manifest, ident)
-
如果
fallback
不是 null 也不是 undefined- 將
referenceOrAlias
設定為fallback
- 將
-
-
-
-
如果
referenceOrAlias
仍然是 undefined,則- 擲出解析錯誤
-
如果
referenceOrAlias
仍然是 null,則-
註:這表示
parentPkg
對ident
有未滿足的同儕相依性 -
擲出解析錯誤
-
-
否則,如果
referenceOrAlias
是陣列,則-
讓
alias
成為referenceOrAlias
-
讓
dependencyPkg
成為GET_PACKAGE
(manifest, alias)
-
傳回
path.resolve(manifest.dirPath, dependencyPkg.packageLocation, modulePath)
-
-
否則,
-
讓
reference
成為referenceOrAlias
-
讓
dependencyPkg
成為GET_PACKAGE
(manifest, {ident, reference})
-
傳回
path.resolve(manifest.dirPath, dependencyPkg.packageLocation, modulePath)
-
GET_PACKAGE
GET_PACKAGE(manifest, locator)
-
讓
referenceMap
成為parentPkg.packageRegistryData
中由locator.ident
參照的項目 -
讓
pkg
成為referenceMap
中由locator.reference
參照的項目 -
傳回
pkg
- 注意:
pkg
在此處不能為 undefined;任何 Plug'n'Play 資料表中所引用的所有套件 必須 在packageRegistryData
內有對應的項目。
- 注意:
FIND_LOCATOR
FIND_LOCATOR(manifest, moduleUrl)
注意:此處所描述的演算法相當低效。在讀取清單時,您應確保準備好更適合此任務的資料結構。
-
令
bestLength
為 0 -
令
bestLocator
為 null -
令
relativeUrl
為manifest
與moduleUrl
之間的相對路徑- 注意:相對路徑不得以
./
開頭;若有需要,請將其移除
- 注意:相對路徑不得以
-
如果
relativeUrl
符合manifest.ignorePatternData
,則- 傳回 null
-
令
relativeUrlWithDot
為relativeUrl
,並視需要加上./
或../
前綴 -
對於
manifest.packageRegistryData
中的每個referenceMap
值-
對於
referenceMap
中的每個registryPkg
值-
如果
registryPkg.discardFromLookup
不為 true,則-
如果
registryPkg.packageLocation.length
大於bestLength
,則-
如果
relativeUrl
以registryPkg.packageLocation
開頭,則-
將
bestLength
設為registryPkg.packageLocation.length
-
將
bestLocator
設為目前的registryPkg
定位器
-
-
-
-
-
-
傳回
bestLocator
RESOLVE_VIA_FALLBACK
RESOLVE_VIA_FALLBACK(manifest, ident)
-
令
topLevelPkg
為GET_PACKAGE
(manifest, {null, null})
-
令
referenceOrAlias
為topLevelPkg.packageDependencies
中由ident
參照的項目 -
如果
referenceOrAlias
已定義,則- 立即傳回
-
否則,
-
令
referenceOrAlias
為manifest.fallbackPool
中由ident
參照的項目 -
立即傳回,無論其是否已定義
-
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)
-
讓
manifest
為 null -
讓
directoryPath
為url
的目錄 -
讓
pnpPath
為directoryPath
與/.pnp.cjs
連接 -
如果
pnpPath
存在於檔案系統中,則-
讓
pnpDataPath
為directoryPath
與/.pnp.data.json
連接 -
將
manifest
設定為JSON.parse(readFile(pnpDataPath))
-
將
manifest.dirPath
設定為directoryPath
-
傳回
manifest
-
-
否則,如果
directoryPath
為/
,則- 傳回 null
-
否則,
- 傳回
FIND_PNP_MANIFEST
(directoryPath)
- 傳回
PARSE_BARE_IDENTIFIER
PARSE_BARE_IDENTIFIER(specifier)
-
如果
specifier
以「@」開頭,則-
如果
specifier
不包含「/」分隔符號,則- 擲回錯誤
-
否則,
- 將
ident
設定為specifier
的子字串,直到第二個「/」分隔符號或字串結尾,以先發生的為準
- 將
-
-
否則,
- 將
ident
設定為specifier
的子字串,直到第一個「/」分隔符號或字串結尾,以先發生的為準
- 將
-
將
modulePath
設定為從ident.length
開始的specifier
子字串 -
傳回
{ident, modulePath}