PnP API
Обзор
Помимо простой стратегии установки, Plug'n'Play также предоставляет API, позволяющий просматривать дерево зависимостей во время выполнения.
Структуры данных
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:bar@1.2.3").Если
null, указанная зависимость вообще недоступна. Это обычно происходит, когда зависимость "peer" пакета не была предоставлена непосредственным родителем в дереве зависимостей.
Поле packagePeers, если присутствует, указывает, какие зависимости имеют принудительный контракт на использование ровно одного и того же экземпляра, как и у пакета, от которого они зависят. Это поле редко используется в контексте чистого PnP (потому что наши гарантии создания экземпляров строже и предсказуемее), но необходимо для правильного создания каталога node_modules из карты PnP.
Поле linkType полезно только в определенных случаях — оно описывает, был ли производитель API PnP попрошен сделать пакет доступным через жесткую ссылку (в этом случае все поле 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')
Встроенный модуль module расширяется при работе в рамках API PnP с одной дополнительной функцией:
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 фактически не требуется в большинстве случаев, и мы можем сделать то же самое с помощью просто createRequire благодаря его функции resolve:
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. Он предоставляет константы и функции, описанные в остальной части документа.
Обратите внимание, что мы зарезервировали имя пакета pnpapi на npm-регистре, поэтому нет риска, что кто-то сможет завладеть этим именем с неблаговидными намерениями. Мы можем использовать его позже для обеспечения полифилла для сред, не поддерживающих PnP (чтобы вы могли использовать API PnP независимо от того, был ли проект установлен с помощью 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 возвращает всю информацию, хранящуюся в API PnP для данного пакета.
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 — возможно, самая важная функция, экспортируемая API PnP. Принимая запрос (который может быть простым спецификатором, как lodash, или относительным/абсолютным путём, как ./foo.js), и путь к файлу, который сделал этот запрос, API PnP вернёт неквалифицированное разрешение.
Например, следующее:
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 в некоторые проекты, хотя это необязательно, если у вас уже есть что-то подходящее.
Например, resolveUnqualified не требуется с enhanced-resolved, используемым Webpack, потому что он уже реализует свою собственную логику, содержащуюся в 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, если у вас уже есть система разрешений, которая просто нуждается в поддержке Plug'n'Play.
Например, следующее:
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.js. Неквалифицированные разрешения могут быть вычислены статически без обращения к файловой системе, но могут разрешать только относительные пути и простые спецификаторы (например, lodash); они никогда не будут разрешать расширения файлов или индексы папок. Напротив, квалифицированные разрешения готовы к использованию для доступа к файловой системе.
Неквалифицированные разрешения — это основа API Plug'n'Play; они представляют данные, которые невозможно получить иным способом. Если вы хотите интегрировать Plug'n'Play в свой решатель, они, вероятно, то, что вам нужно. С другой стороны, полностью квалифицированные разрешения полезны, если вы работаете с API PnP разово и просто хотите получить некоторую информацию о конкретном файле или пакете.
Два отличных варианта для разных сценариев 🙂
Доступ к файлам
Пути, возвращаемые в структурах PackageInformation, находятся в родном формате (POSIX на Linux/OSX и Win32 на Windows), но они могут ссылаться на файлы, находящиеся за пределами стандартной файловой системы. Это особенно верно для 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);
}
© 2016–present Yarn Contributors
Licensed under the BSD License.
https://v3.yarnpkg.com/advanced/pnpapi