Руководство
Создание портативных пакетов имеет огромное значение, поскольку это гарантирует, что ваши пользователи получат оптимальный опыт независимо от их менеджера пакетов.
Чтобы помочь в этом, на этой странице подробно описана актуальная коллекция лучших практик, которых вы должны придерживаться, чтобы ваш пакет работал без проблем на всех трех основных менеджерах пакетов (Yarn, pnpm и npm), а также объяснения, если вы хотите узнать больше.
- Пакеты должны требовать только то, что формально указано в их зависимостях
- Модули не должны жестко кодировать пути
node_modulesдля доступа к другим модулям - Пользовательские скрипты не должны жестко кодировать папку
node_modules/.bin - Опубликованные пакеты должны избегать использования
npm runв своих скриптах - Пакеты никогда не должны записывать данные в собственную папку за пределами стадии postinstall
- Пакеты должны использовать скрипт
prepackдля генерации файлов dist перед публикацией
Пакеты должны требовать только то, что формально указано в их зависимостях
Почему? Потому что в противном случае ваш пакет будет уязвим к непредсказуемому [подниманию](/advanced/lexicon#hoisting), что приведет к непредсказуемым сбоям у некоторых пользователей в зависимости от других пакетов, которые они будут использовать. Щелкните этот абзац, чтобы развернуть его и прочитать подробный пример проблем, обычно вызываемых неправильным поднимижением.
Представьте, что Алиса использует Babel. Babel зависит от вспомогательного пакета, который сам зависит от старой версии Lodash. Поскольку вспомогательный пакет уже зависит от Lodash, Боб, разработчик Babel, решил использовать Lodash без формального объявления его в Babel.
Из-за поднимания Lodash окажется вверху, дерево станет похожим на это:
Пока всё хорошо: вспомогательный пакет по-прежнему может требовать Lodash, а теперь и Babel тоже. Отлично! Теперь представьте, что Алиса также добавляет Gatsby, что изменит дерево зависимостей следующим образом:
Теперь поднимание становится более интересным — поскольку Babel не объявляет зависимость формально, могут произойти два различных варианта расположения. Первый практически идентичен тому, что у нас было раньше, и в этом случае всё будет работать нормально:
Но такой же вероятен и второй вариант! И именно тогда возникают трудности:
Сначала проверим, что это расположение допустимо: Gatsby по-прежнему получает свою зависимость Lodash 4, вспомогательный пакет Babel по-прежнему получает Lodash 1, а сам Babel по-прежнему получает вспомогательный пакет, как и раньше. Но подождите, кое-что изменилось! Babel больше не будет обращаться к Lodash 1! Вместо этого он получит копию Lodash 4, предоставленную Gatsby, которая, вероятно, несовместима с тем, что Babel изначально ожидал. В лучшем случае приложение аварийно завершится, в худшем — оно молча пройдёт и выдаст некорректные результаты.
Если бы Babel определил Lodash 1 как свою собственную зависимость, менеджер пакетов смог бы закодировать это ограничение и гарантировать, что требование будет выполнено независимо от поднятия.
Решение: В большинстве случаев (когда отсутствующая зависимость — вспомогательный пакет) исправление заключается всего лишь в добавлении отсутствующей записи в поле dependencies. Хотя часто встречаются более сложные случаи:
Если ваш пакет — плагин (например,
babel-plugin-transform-commonjs) и отсутствующая зависимость — ядро (например,babel-core), вам нужно будет вместо этого зарегистрировать зависимость в полеpeerDependencies.Если ваш пакет автоматически загружает плагины (например,
eslint), зависимость от плагинов очевидно не подходит, так как вы не можете разумно перечислить все плагины. Вместо этого вы должны использовать функциюcreateRequire(или её полифил), чтобы загружать плагины от имени файла конфигурации, который перечисляет плагины для загрузки — будь то package.json или пользовательский файл, например.eslintrc.js.Если ваш пакет требует зависимость только в определённых случаях, контролируемых пользователем (например,
mikro-orm, который зависит только отsqlite3, если потребитель действительно использует базу данных SQLite3), используйте полеpeerDependenciesMeta, чтобы объявить зависимость от плагина как необязательную и подавить любое предупреждение при её отсутствии.-
Если ваш пакет — метапакет утилит (например, Next.js, который сам зависит от Webpack, чтобы его пользователи не должны были этого делать), ситуация немного сложнее, и у вас есть два варианта:
Предпочтительный вариант — указать зависимость (в случае Next.js,
webpack) как обычную зависимость и зависимость от плагина. Yarn будет интерпретировать эту схему как «зависимость от плагина по умолчанию», что позволит вашим пользователям взять на себя ответственность за пакет Webpack, если им это потребуется, при этом менеджер пакетов сможет выдать предупреждение, если предоставленная версия несовместима с той, которую ожидает ваш пакет.Альтернативный вариант — вместо этого переэкспортировать зависимость в состав вашей публичной API. Например, Next может экспортировать файл
next/webpack, содержащий толькоmodule.exports = require('webpack'), а пользователи будут требовать его вместо обычного модуляwebpack. Однако этот подход не рекомендуется, потому что он не будет работать с плагинами, которые ожидают, что Webpack будет зависимостью от плагина (они не будут знать, что им нужно использовать этот модульnext/webpackвместо него).
Модули не должны жестко кодировать пути node_modules для доступа к другим модулям
Почему? Поднимание делает невозможным быть уверенным, что структура папки node_modules всегда будет одинаковой. Фактически, в зависимости от конкретной стратегии установки папки node_modules могут даже не существовать.
Решение: Если вам нужно получить доступ к файлам одной из ваших зависимостей через API fs (например, для чтения файла package.json) зависимостей), просто используйте require.resolve для получения пути без необходимости делать предположения о расположении зависимостей:
const fs = require(`fs`);
const data = fs.readFileSync(require.resolve(`my-dep/package.json`));Если вам нужно получить доступ к зависимостям зависимостей (мы действительно не рекомендуем этого, но в некоторых крайних случаях это может потребоваться), вместо жесткого кодирования пути node_modules, используйте функцию createRequire:
const {createRequire} = require(`module`);
const firstDepReq = createRequire(require.resolve(`my-dep/package.json`));
const secondDep = firstDepReq(`transitive-dep`);Обратите внимание, что, хотя createRequire доступен с Node 12+, полифил существует под именем create-require.
Пользовательские скрипты не должны жестко кодировать папку node_modules/.bin
Почему? Папка .bin — деталь реализации и может вообще не существовать в зависимости от стратегии установки.
Решение: Если вы пишете скрипт, вы можете просто указать бинарный файл по имени! Так что вместо node_modules/.bin/jest -w, лучше использовать jest -w, что будет работать без проблем. Если по какой-то причине jest недоступен, проверьте, что текущий пакет должным образом определяет его как зависимость.
Иногда вам могут потребоваться более сложные задачи, например, если вы хотите запустить скрипт со специфическими флагами Node. В зависимости от контекста, мы рекомендуем передавать параметры через переменную среды NODE_OPTIONS вместо командной строки, но если это невозможно, вы можете использовать yarn bin <name> для получения указанного пути к бинарному файлу:
yarn node --inspect $(yarn bin jest)Обратите внимание, что в этом конкретном случае yarn run также поддерживает флаг --inspect, так что вы можете написать:
yarn run --inspect jest
Опубликованные пакеты должны избегать использования npm run в своих скриптах
Почему? Это непросто… в основе всего лежит: менеджеры пакетов не взаимозаменяемы. Использование одного менеджера пакетов в проекте, установленным другим менеджером, чревато проблемами, так как они используют различные настройки и правила. Например, Yarn предлагает систему хуков, которая позволяет пользователям отслеживать, какие скрипты выполняются и сколько времени они занимают. Поскольку npm run не будет знать, как вызывать эти хуки, они будут игнорироваться, что приведёт к неудобствам для ваших пользователей.
Решение: Хотя это не самый эстетичный вариант, на данный момент наиболее портативным является простое замещение npm run <name> (или yarn run <name>) в скриптах postinstall и производных от следующего:
$npm_execpath run <name>Переменная среды $npm_execpath будет заменена соответствующим бинарным файлом в зависимости от менеджера пакетов, который будут использовать ваши пользователи. Yarn также поддерживает вызов run <name> без упоминания менеджера пакетов, но на данный момент другие менеджеры пакетов этого не делают.
Пакеты никогда не должны записывать данные в собственную папку за пределами стадии postinstall
Почему? В зависимости от стратегии установки пакеты могут храниться в хранилищах только для чтения, где записи будут отклоняться. Это особенно верно при использовании «системных глобальных» хранилищ, где изменение источников одного пакета может привести к повреждению всех проектов, зависящих от него на одном компьютере.
Решение: Просто записывайте в другую директорию, а не в свою собственную. Любая директория подойдёт, но очень распространённой практикой является использование папки node_modules/.cache для хранения данных кэша — это, например, делают Babel, Webpack и другие.
Если вам абсолютно необходимо записать данные в папку с исходным кодом вашего пакета (но на самом деле мы никогда раньше не сталкивались с таким случаем), у вас всё ещё есть возможность использовать preferUnplugged, чтобы указать Yarn на отключение оптимизаций для вашего пакета и сохранение его в собственном локальном копии проекта, где вы сможете его изменять по своему желанию.
Пакеты должны использовать скрипт prepack для генерации файлов dist перед публикацией
Почему? Исходный npm поддерживал множество различных скриптов. Настолько много, что стало очень сложно понять, какой скрипт нужно использовать в каком контексте. В частности, очень тонкие различия между скриптами prepack, prepare, prepublish, и prepublish-only привели к тому, что многие использовали неправильный скрипт в неправильном контексте. По этой причине Yarn 2 устарел большинство скриптов и объединил их вокруг ограниченного набора переносимых скриптов.
Решение: Всегда используйте скрипт prepack, если вы хотите сгенерировать файлы dist перед публикацией вашего пакета. Он будет вызван перед вызовом yarn pack (который сам вызывается перед вызовом yarn npm publish), при клонировании вашего репозитория git в качестве зависимости git и каждый раз, когда вы будете запускать yarn prepack. Что касается prepublish, никогда не используйте его с побочными эффектами — его единственная задача заключается в запуске тестов перед этапом публикации.
© 2016–present Yarn Contributors
Licensed under the BSD License.
https://v3.yarnpkg.com/advanced/rulebook