.pnpmfile.mjs
pnpm дозволяє вам підключатися безпосередньо до процесу встановлення за допомогою спеціальних функцій (хуків). Хуки можна оголошувати у файлі з назвою .pnpmfile.mjs (ESM) або .pnpmfile.cjs (CommonJS).
Стандартно .pnpmfile.mjs має бути розташований у тій же теці, що і файл блокування. Наприклад, у робочому просторі зі спільним файлом блокування, .pnpmfile.mjs повинен знаходитись у корені монорепо.
Хуки
TL;DR
| Функція хука | Процес | Використання |
|---|---|---|
hooks.readPackage(pkg, context): pkg | Викликається після того, як pnpm проаналізує маніфест пакунків залежності | Дозволяє змінювати файл package.json залежності. |
hooks.afterAllResolved(lockfile, context): lockfile | Викликається після розвʼязання залежностей. | Дозволяє змінювати файл блокування. |
hooks.beforePacking(pkg): pkg | Викликається перед створенням tar-архіву під час пакування/публікації | Дозволяє налаштувати опублікований package.json |
resolvers | Викликається під час виконання резолюції пакунка. | Дозволяє реєструвати власні резолвери пакунків. |
fetchers | Викликається під час отримання пакунка. | Дозволяє реєструвати власні фетчери пакунків. |
hooks.readPackage(pkg, context): pkg | Promise<pkg>
Дозволяє змінювати package.json залежності після аналізу і до розвʼязання. Ці мутації не зберігаються у файловій системі, проте вони впливають на те, що буде отримано у файлі блокування, а отже, і на те, що буде встановлено.
Зауважте, що вам потрібно буде видалити pnpm-lock.yaml, якщо ви вже розвʼязали залежність, яку ви хочете змінити.
Якщо вам потрібно внести зміни до package.json, збереженого у файловій системі, вам потрібно скористатися командою pnpm patch і виправити файл package.json. Це може бути корисно, наприклад, якщо ви хочете видалити поле bin у залежності.
Аргументи
pkg– маніфест пакунка. Або відповідь з реєстру, або вмістpackage.json.context— обʼєкт Context для кроку. Метод#log(msg)дозволяє використовувати журнал налагодження для кроку.
Використання
Приклад .pnpmfile.mjs (змінює dependencies залежності):
function readPackage(pkg, context) {
// Перевизначимо маніфест foo@1.x, завантаживши його з реєстру
if (pkg.name === 'foo' && pkg.version.startsWith('1.')) {
// Заміна bar@x.x.x with bar@2.0.0
pkg.dependencies = {
...pkg.dependencies,
bar: '^2.0.0'
}
context.log('bar@1 => bar@2 in dependencies of foo')
}
// Це призведе до того, що всі пакунки, які використовують
// baz@x.x.x, почнуть використовувати baz@1.2.3
if (pkg.dependencies.baz) {
pkg.dependencies.baz = '1.2.3';
}
return pkg
}
export const hooks = {
readPackage
}
Відомі обмеження
Видалення поля scripts з маніфесту залежності за допомогою readPackage не завадить pnpm зібрати залежність. При побудові залежності pnpm зчитує package.json пакунка з архіву пакунка, на який хук не впливає. Щоб проігнорувати збірку пакунка, скористайтеся полем allowBuilds.
hooks.updateConfig(config): config | Promise<config>
Додано у: v10.8.0
Дозволяє змінювати параметри конфігурації, які використовуються pnpm. Цей хук є найбільш корисним у поєднанні з configDependencies, що дозволяє вам ділитися та повторно використовувати налаштування в різних репозиторіях Git.
Наприклад, @pnpm/plugin-better-defaults використовує хук updateConfig для застосування спеціального набору рекомендованих параметрів.
Приклад використання
export const hooks = {
updateConfig (config) {
return Object.assign(config, {
enablePrePostScripts: false,
optimisticRepeatInstall: true,
resolutionMode: 'lowest-direct',
verifyDepsBeforeRun: 'install',
})
}
}
hooks.afterAllResolved(lockfile, context): lockfile | Promise<lockfile>
Дозволяє змінювати вивід файлу блокування перед його серіалізацією.
Аргументи
lockfile— Обʼєкт резолюції файлу блокування, який серіалізовано уpnpm-lock.yaml.context— обʼєкт Context для кроку. Метод#log(msg)дозволяє використовувати журнал налагодження для кроку.
Приклад використання
function afterAllResolved(lockfile, context) {
// ...
return lockfile
}
export const hooks = {
afterAllResolved
}
Відомі обмеження
Їх немає — все, що можна зробити з файлом блокування, можна змінити за допомогою цієї функції, і ви навіть можете розширити функціональність файлу блокування.
hooks.beforePacking(pkg): pkg | Promise<pkg>
Додано у: v10.28.0
Дозволяє змінити маніфест package.json перед його пакуванням у tar-архів під час pnpm pack або pnpm publish. Це корисно для налаштування опублікованого пакунка без впливу на вашу локальну розробку package.json.
На відміну від hooks.readPackage, який змінює спосіб розвʼязання залежностей під час встановлення, beforePacking впливає лише на вміст архіву, який публікується.
Аргументи
pkg– Об’єкт маніфесту пакунка, який буде включено до опублікованого tar-архіву.
Приклад використання
function beforePacking(pkg) {
// Видалити поля, призначені лише для розробки, з опублікованого пакунка
delete pkg.devDependencies
delete pkg.scripts.test
// Додати метадані публікації
pkg.publishedAt = new Date().toISOString()
// Змінити експорт пакунків для production
if (pkg.name === 'my-package') {
pkg.main = './dist/index.js'
}
return pkg
}
export const hooks = {
beforePacking
}
Зміни, зроблені цим хуком, впливають тільки на package.json всередині архіву. Ваш локальний файл package.json залишається незмінним.
hooks.preResolution(options): Promise<void>
Цей хук буде виконаний після читання та аналізу файлів блокування проєкту, але перед розвʼязанням залежностей. Це дозволяє змінювати обʼєкти файлу блокування.
Аргументи
options.existsCurrentLockfile— булеве значення, яке є істинним, якщо файл блокування за адресоюnode_modules/.pnpm/lock.yamlіснує.options.currentLockfile— Обʼєкт файлу блокування зnode_modules/.pnpm/lock.yaml.options.existsNonEmptyWantedLockfile— булеве значення, яке є істинним, якщо файл блокування за адресоюpnpm-lock.yamlіснує.options.wantedLockfile— Обʼєкт файлу блокування зpnpm-lock.yaml.options.lockfileDir— Тека, де знаходиться потрібний файл блокування.options.storeDir— Розташування теки store.options.registries— Зіставлення областей видимості з URL-адресами реєстрів.
hooks.importPackage(destinationDir, options): Promise<string | undefined>
Цей хук дозволяє змінити спосіб запису пакунків до node_modules. Значення, що повертається, є необовʼязковим і вказує, який метод було використано для імпорту залежності, наприклад: clone, hardlink.
Аргументи
destinationDir— Тека призначення, куди має бути записано пакунок.options.disableRelinkLocalDirDepsoptions.filesMapoptions.forceoptions.resolvedFromoptions.keepModulesDir
hooks.fetchers
hooks.fetchers було видалено. Використовуйте натомість фетчери верхнього рівня. Інформацію про новий API дивіться у розділі Власні фетчери.
Пошук
Додано у: v10.16.0
Функції пошуку використовуються з pnpm list та pnpm why через прапорець --find-by.
Приклад:
export const finders = {
react17: (ctx) => {
return ctx.readManifest().peerDependencies?.react === "^17.0.0"
}
}
Використання:
pnpm why --find-by=react17
Дивіться Пошук для отримання додаткової інформації.
Власні резолвери та фетчери
Додано у: v11.0.0
За допомогою власних резолверів та фетчерів можна реалізувати власну логіку резолюції та отримання пакунків для нових схем ідентифікаторів пакунків (наприклад, my-protocol:package-name). Вони реєструються як експорти верхнього рівня в .pnpmfile.cjs:
module.exports = {
resolvers: [customResolver1, customResolver2],
fetchers: [customFetcher1, customFetcher2],
}
Інтерфейси TypeScript
interface CustomResolver {
canResolve?: (wantedDependency: WantedDependency) => boolean | Promise<boolean>
resolve?: (wantedDependency: WantedDependency, opts: ResolveOptions) => ResolveResult | Promise<ResolveResult>
shouldRefreshResolution?: (depPath: string, pkgSnapshot: PackageSnapshot) => boolean | Promise<boolean>
}
interface CustomFetcher {
canFetch?: (pkgId: string, resolution: Resolution) => boolean | Promise<boolean>
fetch?: (cafs: Cafs, resolution: Resolution, opts: FetchOptions, fetchers: Fetchers) => FetchResult | Promise<FetchResult>
}
Власні резолвери
Власні резолвери перетворюють дескриптори пакунків (наприклад, foo@^1.0.0) на резолюції, які зберігаються у файлі блокування.
Інтерфейс резолвера
Власний резолвер — це об’єкт, який може реалізовувати будь-яку комбінацію таких методів:
canResolve(wantedDependency): boolean | Promise<boolean>
Визначає, чи може цей резолвер розвʼязати задану залежність.
Аргументи:
wantedDependency— Об'єкт з:alias– Назва пакунка або псевдонім, з яким він зʼявляється в package.jsonbareSpecifier— Діапазон версій, URL-адреса git, шлях до файлу або інший специфікатор
Повертає: true якщо цей резолвер може обробити пакунок, false – якщо ні. Визначає, чи буде викликано resolve.
resolve(wantedDependency, opts): ResolveResult | Promise<ResolveResult>
Визначає необхідну залежність від конкретних метаданих пакунка та інформації про її визначення.
Аргументи:
wantedDependency— Бажана залежність (те саме, що йcanResolve)opts— Обʼєкт з:lockfileDir– Тека, що містить файл блокуванняprojectDir— Коренева тека проєктуpreferredVersions— Мапа назв пакунків та їх бажаних версій
Повертає: Обʼєкт з:
id— Унікальний ідентифікатор пакунка (наприклад,'custom-pkg@1.0.0')resolution— Метадані резолюції. Це можуть бути:- Стандартна резолюція, напр
{ tarball: 'https://...', integrity: '...' } - Власна резолюція:
{ type: 'custom:cdn', url: '...' }
- Стандартна резолюція, напр
Власні резолюції можуть оброблятись відпвоідним власним фетчером.
Власні резолюції повинні використовувати префікс custom: в полі типу (напр., custom:cdn, custom:artifactory) щоб відрізняти їх від вбудованих типів резолюцій.
shouldRefreshResolution(depPath, pkgSnapshot): boolean | Promise<boolean>
Отримання true запускає процес визначення резолюцій всіх пакунків, пропускаючи оптимізацію "Lockfile is up to date". Це є корисним для реалізації інвалідування кешу на основі часу або іншої власної логіки повторного визначення резолюцій.
Аргументи:
depPath— рядок ідентифікатора пакунка (наприклад,lodash@4.17.21)pkgSnapshot— Запис в lockfile для цього пакунка, що забезпечує прямий доступ до резолюцій, залежностей тощо.
Повертає: true для примусового повторного визначення резолюцій, інакше — false.
shouldRefreshResolution пропускається під час встановлення з замороженим файлом блокування, оскільки в цьому режимі резолюції не допускаються.
Власні фетчери
Власні фетчери повністю відповідають за завантаження власних типів пакунків, завантажуючи вміст пакунків із власних джерел та зберігаючи його у файловій системі pnpm з адресацією за вмістом.
Інтерфейс фетчера
Власний фетчер — це об’єкт, який може впроваджувати наступні методи:
canFetch(pkgId, resolution): boolean | Promise<boolean>
Визначає, чи може цей фетчер отримати пакунок з вказаною резолюцією.
Аргументи:
pkgId— Унікальний ідентифікатор пакунка з етапу резолюціїresolution– Обʼєкт резолюції з методуresolveрезолвера
Повертає: true якщо цей фетчер може отримати пакунок, false — якщо ні.
fetch(cafs, resolution, opts, fetchers): FetchResult | Promise<FetchResult>
Отримує файли пакунка та повертає метадані отриманого пакунка.
Аргументи:
cafs— Інтерфейс файлової системи з адресацією вмісту для зберігання файлівresolution– Об’єкт резолюції (такий самий, як передано доcanFetch)opts— Параметри отримання включають:lockfileDir– Тека, що містить файл блокуванняfilesIndexFile— Шлях до індексу файлівonStart— Опціональний зворотний виклик, що спрацьовує на початку отриманняonProgress— Опціональний зворотний виклик до progress
fetchers— Обʼєкт, що містить стандартні фетчери pnpm для делегування:remoteTarball— Фетчер для отримання віддалених tar-архівівlocalTarball— Фетчер для отримання локальних tar-архівівgitHostedTarball— Фетчер для отримання tar-архівів з GitHub/GitLab/Bitbucketdirectory— Фетчер для отримання локальних текgit— Фетчер для git-репозиторіїв
Повертає: Обʼєкт з:
filesIndex— Мапа відносних шляхів до файлів та їхніх фізичних розташувань. Для віддалених пакунків це шляхи у сховищі pnpm з адресацією за вмістом (CAFS, content-addressable store). Для локальних пакунків (колиlocal: true) це абсолютні шляхи до файлів на диску.manifest— Опціонально. Файл package.json з отриманого пакунка. Якщо не вказано, pnpm прочитає його з диску за потреби. Це дозволяє уникнути додаткової операції вводу-виводу і рекомендується, якщо дані маніфесту вже доступні (наприклад, якщо вони були проаналізовані під час отримання).requiresBuild— Булеве значення, яке вказує, чи містить пакунок скрипти збірки, які потрібно виконати. Встановлює значенняtrue, якщо пакунок містить скриптиpreinstall,installабоpostinstall, або містить файлиbinding.gypабо.hooks/. Стандартні фетчери визначають це автоматично на основі маніфесту та списку файлів.local— Опціонально. Встановлює значенняtrue, щоб завантажити пакунок безпосередньо з диска без копіювання до сховища pnpm. Колиtrue,filesIndexмає містити абсолютні шляхи до файлів на диску, і pnpm жорстко звʼяже їх зnode_modulesзамість копіювання. Ось так фетчер тек обробляє локальні залежності (наприклад,file:../my-package).
Власні фетчери можуть виконувати делегування стандартним фетчерам pnpm за допомогою параметра fetchers.
Приклади використання
Простий власний резолвер
У цьому прикладі показано власний резолвер, який розпізнає пакунки з власного реєстру:
const customResolver = {
// Обробляти лише пакунки з областю дії @company
canResolve: (wantedDependency) => {
return wantedDependency.alias.startsWith('@company/')
},
resolve: async (wantedDependency, opts) => {
// Отримати метадані з власного репозиторію
const response = await fetch(
`https://custom-registry.company.com/${wantedDependency.alias}/${wantedDependency.bareSpecifier}`
)
const metadata = await response.json()
return {
id: `${metadata.name}@${metadata.version}`,
resolution: {
tarball: metadata.tarballUrl,
integrity: metadata.integrity
}
}
}
}
module.exports = {
resolvers: [customResolver]
}
Власний резолвер та фетчер з shouldRefreshResolution
У цьому прикладі показано, як резолвер і фетчер працюють разом із власним типом резолюції та інвалідуванням кешу за часом:
const customResolver = {
canResolve: (wantedDependency) => {
return wantedDependency.alias.startsWith('company-cdn:')
},
resolve: async (wantedDependency, opts) => {
const actualName = wantedDependency.alias.replace('company-cdn:', '')
const version = await fetchVersionFromCompanyCDN(actualName, wantedDependency.bareSpecifier)
return {
id: `company-cdn:${actualName}@${version}`,
resolution: {
type: 'custom:cdn',
cdnUrl: `https://cdn.company.com/packages/${actualName}/${version}.tgz`,
cachedAt: Date.now(), // Custom metadata for shouldRefreshResolution
},
}
},
shouldRefreshResolution: (depPath, pkgSnapshot) => {
// Перевірити власні метадані, що зберігаються у резолюції
const cachedAt = pkgSnapshot.resolution?.cachedAt
if (cachedAt && Date.now() - cachedAt > 24 * 60 * 60 * 1000) {
return true // Re-resolve if cached more than 24 hours ago
}
return false
},
}
const customFetcher = {
canFetch: (pkgId, resolution) => {
return resolution.type === 'custom:cdn'
},
fetch: async (cafs, resolution, opts, fetchers) => {
// Делегуємо стандартному фетчеру tar-архівів pnpm
const tarballResolution = {
tarball: resolution.cdnUrl,
integrity: resolution.integrity,
}
return fetchers.remoteTarball(cafs, tarballResolution, opts)
},
}
module.exports = {
resolvers: [customResolver],
fetchers: [customFetcher],
}
Простий власний фетчер
У цьому прикладі показано власний фетчер, який завантажує певні пакунки з іншого джерела:
const customFetcher = {
canFetch: (pkgId, resolution) => {
return pkgId.startsWith('@company/')
},
fetch: async (cafs, resolution, opts, fetchers) => {
// Делегуємо фетчеру tar-архівів pnpm змінений URL
const tarballResolution = {
tarball: resolution.tarball.replace(
'https://registry.npmjs.org/',
'https://custom-registry.company.com/'
),
integrity: resolution.integrity
}
return fetchers.remoteTarball(cafs, tarballResolution, opts)
}
}
module.exports = {
fetchers: [customFetcher]
}
Власний тип резолюції з резолвером і фетчером
У цьому прикладі показано, як власний резолвер і фетчер працюють разом із власним типом резолюції:
const customResolver = {
canResolve: (wantedDependency) => {
return wantedDependency.alias.startsWith('@internal/')
},
resolve: async (wantedDependency) => {
return {
id: `${wantedDependency.alias}@${wantedDependency.bareSpecifier}`,
resolution: {
type: 'custom:internal-directory',
directory: `/packages/${wantedDependency.alias}/${wantedDependency.bareSpecifier}`
}
}
}
}
const customFetcher = {
canFetch: (pkgId, resolution) => {
return resolution.type === 'custom:internal-directory'
},
fetch: async (cafs, resolution, opts, fetchers) => {
// Делегуємо фетчеру тек pnpm локальні пакунки
const directoryResolution = {
type: 'directory',
directory: resolution.directory
}
return fetchers.directory(cafs, directoryResolution, opts)
}
}
module.exports = {
resolvers: [customResolver],
fetchers: [customFetcher]
}
Пріоритет та порядок
Коли зареєстровано кілька резолверів, вони перевіряються по порядку. Для розвʼязання буде використано перший резолвер, для якого canResolve повертає true. Те саме стосується і фетчерів: перший фетчер, для якого canFetch повертає true, буде використаний для отримання пакунків.
Спочатку перевіряються власні резолвери, а вже потім — вбудовані резолвери pnpm (npm, git, tarball тощо), що дає вам повний контроль над пошуком пакунків.
Зауваження щодо продуктивності
Функції canResolve(), canFetch() та shouldRefreshResolution() мають бути дешевими перевірками (в ідеалі — синхронними), оскільки вони викликаються для кожної залежності під час розвʼязання.
Повʼязана конфігурація
ignorePnpmfile
- Стандартно: false
- Тип: Boolean
Файл pnpmfile буде проігноровано. Корисно використовувати разом з --ignore-scripts, якщо ви хочете переконатися, що жоден скрипт не буде виконано під час встановлення.
pnpmfile
- Стандартно: ['.pnpmfile.mjs']
- Тип: path[]
- Приклад: ['.pnpm/.pnpmfile.mjs']
Розташування локальних файлів pnpmfile.
globalPnpmfile
- Стандартно: null
- Тип: path
- Приклад: ~/.pnpm/global_pnpmfile.mjs
Розташування глобального pnpmfile. Глобальний pnpmfile використовується всіма проєктами під час встановлення.
Рекомендується використовувати локальні pnpmfiles. Використовуйте глобальний файл pnpmfile лише у тому випадку, якщо ви використовуєте pnpm у проєктах, які не використовують pnpm як основний менеджер пакунків.