跳至主要內容

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 值應解釋為

  • 如果為字串,則該值應當作定位器的參考,其名稱為相依性名稱。

  • 如果為 [字串, 字串] 組合,則該值應當作定位器,其名稱為組合的第一個元素,參考為第二個元素。這通常發生在套件別名(例如 "foo": "npm:bar@1.2.3")。

  • 如果為 null,則指定的相依性完全不可用。這通常發生在套件的對等相依性未由其在相依性樹中的直接父項提供時。

如果 packagePeers 欄位存在,表示哪些相依性強制使用與依賴它們的套件完全相同的實例。此欄位在純 PnP 環境中很少有用(因為我們的實例化保證比這更嚴格且更可預測),但需要從 PnP 地圖正確產生 node_modules 目錄。

linkType 欄位僅在特定情況下才有用 - 它描述 PnP API 的產生者是否被要求透過硬連結讓套件可用(在這種情況下,所有 packageLocation 欄位都由連結器擁有)或軟連結(在這種情況下,packageLocation 欄位表示連結器影響範圍之外的位置)。

執行時期常數

process.versions.pnp

在 PnP 環境下執行時,此值會設定為一個數字,表示正在使用的 PnP 標準版本(與 require('pnpapi').VERSIONS.std 完全相同)。

此值是一種方便的方式,可檢查您是否在 Plug'n'Play 環境(您可以在其中 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')

在 Plug'n'Play 環境下執行時,你的樹狀結構中會出現一個新的內建模組,且會提供給你的所有套件(無論它們是否在相依性中定義它):pnpapi。它會公開本文件其他部分中所述的常數和函式。

請注意,我們已在 npm 註冊表中保留 pnpapi 套件名稱,因此沒有人能夠為了邪惡的目的搶走這個名稱。我們可能會在稍後使用它來提供非 PnP 環境的 polyfill(讓你可以使用 PnP API,無論專案是否透過 PnP 安裝),但目前它仍是一個空的套件。

請注意,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 公開的最重要函數。PnP API 會針對請求(可能是像 lodash 這樣的裸露規格,或像 ./foo.js 這樣的相對/絕對路徑)以及發出請求的檔案路徑,傳回非限定解析。

例如,下列

lodash/uniq

很可能會解析為

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

如你所見,.js 副檔名並未加入。這是因為 限定和非限定解析 之間的差異。如果你必須取得準備好與檔案系統 API 一起使用的路徑,請改用 resolveRequest

請注意,在某些情況下,你可能只有資料夾可以使用作為 issuer 參數。當發生這種情況時,只要在發行者後面加上一個額外的斜線 (/) 即可,以指示 PnP API 發行者是資料夾。

如果請求是內建模組,此函數會傳回 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(...)),但較短。

就像 resolveUnqualifiedresolveRequest 完全是選用的,如果你已經有解析管道,只需要加入 Plug'n'Play 的支援,你可能想要略過它,直接使用較低層級的 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;

重要:此函式並非 Plug'n'Play 規範的一部分,僅作為 Yarn 擴充功能提供。若要使用它,你必須先檢查 VERSIONS 字典是否包含有效的 resolveVirtual 屬性。

resolveVirtual 函式會接受任何路徑作為參數,並傳回移除任何 虛擬元件 的相同路徑。這使得儲存檔案位置的方式更為便攜,只要你不介意在過程中遺失相依樹資訊(透過這些路徑要求檔案會讓它們無法存取其對等相依項)。

合格解析與不合格解析

此文件詳細說明了兩種解析類型:合格和不合格。儘管相似,但它們呈現出不同的特性,使其適用於不同的設定。

合格解析與不合格解析的差異在於 Node 解析本身的怪癖。不合格解析可以在不存取檔案系統的情況下進行靜態運算,但只能解析相對路徑和裸露規格說明(如 lodash);它們永遠不會解析檔案副檔名或資料夾索引。相反地,合格解析已準備好可用於存取檔案系統。

不合格解析是 Plug'n'Play API 的核心;它們代表無法透過任何其他方式取得的資料。如果你正在尋找在解析器中整合 Plug'n'Play,它們可能是你正在尋找的。另一方面,如果你將 PnP API 作為一次性使用,並且只想取得特定檔案或套件的一些資訊,則完全合格解析會很方便。

兩個絕佳選項,適用於兩種不同的使用案例 🙂

存取檔案

PackageInformation 結構中傳回的路徑採用原生格式(因此在 Linux/OSX 上為 Posix,在 Windows 上為 Win32),但它們可能會參考檔案系統外部的檔案。這對於 Yarn 來說尤其如此,它直接從其 zip 檔案中參考套件。

要存取此類檔案,可以使用 @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);
}