Как не опубликовать в npm не те файлы
Не допускайте публикации лишних файлов и секретов в npm. Разберитесь, как взаимодействуют .npmignore, .gitignore и files в package.json, и проверьте архив командой npm pack --dry-run.
Если в пакете есть файл .npmignore, npm перестаёт учитывать .gitignore, когда решает, какие файлы не включать в tarball. Это работает, даже если .npmignore пустой.
Обычно об этом узнают, когда tarball оказывается гораздо больше ожидаемого или когда в реестре появляется dot-файл, который никто не собирался публиковать. Правила игнорирования выглядели корректно и действительно были корректными, пока кто-то не добавил второй ignore-файл.
В статье разобраны ловушки, из-за которых в пакет попадают не те файлы: откат на .gitignore, взаимодействие files и .npmignore, фиксированные списки включений и исключений npm, а также действия на случай, если неудачная версия уже опубликована. Предполагается, что вы прочитали статью How to Create and Publish an npm Package.
Ключевые выводы
- Файл
.npmignore, даже пустой, отключает все правила.gitignore, которые npm иначе применил бы к tarball. - npm читает
.npmignoreи.gitignoreв каждом подкаталоге, поэтому ignore-файл глубоко в дереве может изменить состав публикации. - Встроенные исключения npm не распространяются на файлы
.env, приватные ключи и сертификаты. - Если секрет попал в реестр, первым делом замените его. Отмена публикации не отзывает уже скачанные копии.
- Provenance фиксирует, какой репозиторий, коммит и workflow собрали версию. Он не доказывает, что код безопасен.
Ловушка npmignore: один новый файл отменяет все правила gitignore
Чтобы решить, какие файлы исключить из пакета, npm использует либо .npmignore, либо .gitignore, но никогда оба сразу. Правила исключения файлов из пакета у npm короткие. Если .npmignore нет, npm применяет .gitignore. Как только появляется любой .npmignore, .gitignore перестаёт учитываться. Эти два файла никогда не объединяются.
Типичный повод — результаты сборки. dist/ указан в .gitignore, поэтому npm не включает его в tarball. Вы добавляете .npmignore, чтобы dist/ попал в пакет. Теперь dist/ публикуется, а вместе с ним и всё, что раньше скрывал .gitignore: .env.local, coverage/ и test/fixtures/.
Корневой каталог — не единственное место, куда смотрит npm. Он учитывает оба вида ignore-файлов в любой папке внутри пакета, поэтому случайный .npmignore во вложенной папке может изменить состав публикации, даже если корневую конфигурацию никто не трогал.
До и после: как меняется список файлов
Следующий скрипт создаёт пакет, в котором вся фильтрация выполняется через .gitignore:
mkdir pkg-trap && cd pkg-trap
npm init -y >/dev/null
mkdir -p src dist test/fixtures coverage
echo 'module.exports = 1' > src/index.js
echo 'module.exports = 1' > dist/index.js
echo 'API_KEY=replace-me' > .env.local
echo '{}' > test/fixtures/big.json
echo 'x' > coverage/lcov.info
printf 'dist/\n.env.local\ncoverage/\ntest/fixtures/\n' > .gitignore
npm pack --dry-run
npm выводит следующее (только пути, в сокращённом виде):
package.json
src/index.js
dist/ отсутствует, поэтому вы добавляете однострочный .npmignore, чтобы публиковать сборку и исключить исходники:
echo 'src/' > .npmignore
npm pack --dry-run
package.json
- src/index.js
+ dist/index.js
+ .env.local
+ coverage/lcov.info
+ test/fixtures/big.json
Решение — перенести в .npmignore все правила, которые по-прежнему нужны:
printf 'src/\n.env*\ncoverage/\ntest/\n*.map\n' > .npmignore
files или .npmignore: белый список или чёрный
Поле files в package.json работает как белый список (allow-list), а .npmignore — как чёрный (deny-list). Если использовать оба механизма, ни один файл в отдельности не покажет, что на самом деле содержит пакет.
| Механизм | Модель | Новый файл, о котором вы забыли |
|---|---|---|
files | Белый список | Не попадает в пакет |
.npmignore | Чёрный список | Публикуется |
.gitignore (запасной вариант) | Чёрный список | Публикуется, если он не указан в .gitignore |
Именно на пересечении files и .npmignore и начинаются проблемы. Согласно документации по package.json, .npmignore в корне пакета не может исключить ничего из того, что добавлено через files. А .npmignore во вложенной папке — может:
{ "name": "my-lib", "version": "1.0.0", "main": "dist/index.js", "files": ["dist"] }
# dist/.npmignore
*.map
Здесь files включает dist/, а вложенный ignore-файл отфильтровывает из него sourcemap-файлы. Теперь один список определяется двумя файлами. Инструменты сборки, которые сами создают выходные папки, усугубляют ситуацию, поэтому проверяйте содержимое dist/, если следуете нашему руководству по pnpm. Самое простое правило — выбрать один механизм и удалить другой.
Что npm включает и исключает всегда
Некоторые файлы включаются или исключаются независимо от настроек. Согласно документации по package.json, npm всегда включает package.json, README, LICENSE или LICENCE, файл, указанный в main, и все файлы, указанные в bin. npm всегда исключает каталоги систем контроля версий, .npmrc, node_modules и служебные файлы редакторов. Секретов в этом списке нет.
| Всегда включаются | Всегда исключаются (неполный список) | Не исключаются, если не указать явно |
|---|---|---|
package.json, README (и варианты), LICENSE / LICENCE, файл из main, файл(ы) из bin | .git, .npmrc, node_modules (кроме bundled-зависимостей), .DS_Store, npm-debug.log, config.gypi, .*.swp, *.orig, package-lock.json | .env, .env.local, *.pem, *.key, фикстуры, coverage |
Добавьте проверку для правого столбца:
npm pack --dry-run 2>&1 | grep -E '\.env|\.pem$|\.key$' && { echo "STOP: secrets in tarball"; exit 1; }
Запускайте npm pack —dry-run перед каждой публикацией
npm pack --dry-run выводит точный список файлов, которые npm опубликовал бы, не создавая tarball, поэтому выявляет все описанные выше ловушки. Запускайте эту команду после любого изменения ignore-файлов, поля files или конфигурации сборки и добавьте её в CI перед npm publish. Базовый процесс публикации описан в статье How to Create and Publish an npm Package.
npm pack --dry-run
Что делать, если не те файлы уже опубликованы
Если секрет попал в реестр, первым делом замените его и считайте его навсегда публичным. Отмена публикации версии не удаляет копии, которые уже были скачаны или зеркалированы. После этого наведите порядок в реестре:
- Замените (ротируйте) учётные данные у их поставщика.
- Удалите неудачную версию или пометьте её. Согласно политике отмены публикации npm, отменить публикацию можно в течение 72 часов после неё, если от этой версии не зависит ни один другой публичный пакет. По истечении этого срока отменить публикацию можно только для пакетов, соответствующих критериям политики. В остальных случаях используйте
npm deprecate. - Выпустите чистый патч. Использованный номер версии нельзя использовать повторно, даже после отмены публикации.
# Within 72 hours, subject to npm's unpublish policy
npm unpublish my-lib@1.4.2
# Otherwise
npm deprecate my-lib@1.4.2 "Contains a leaked file. Upgrade to 1.4.3."
# Fix the ignore rules, confirm, republish
npm pack --dry-run
npm version patch
npm publish
Provenance подтверждает происхождение, а не безопасность
Provenance-аттестация npm фиксирует, какой репозиторий, коммит и CI-workflow собрали версию пакета. Она не показывает, что код в этом коммите безопасен или что tarball содержит только те файлы, которые вы планировали опубликовать. В GitHub Actions для workflow требуется разрешение id-token: write:
name: publish
on:
release:
types: [published]
permissions:
contents: read
id-token: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
registry-url: https://registry.npmjs.org
- run: npm ci
- run: npm pack --dry-run
- run: npm publish
Этот workflow предполагает, что вы настроили доверенную публикацию (trusted publishing). Для доверенной публикации нужны npm CLI 11.5.1 или новее и Node 22.14.0 или новее; provenance при этом формируется автоматически, поэтому задаче не нужны ни --provenance, ни NODE_AUTH_TOKEN. Если вы публикуете с помощью токена, для npm publish --provenance требуется npm 9.5.0 или новее. В обоих случаях поле repository в package.json должно точно совпадать с репозиторием, из которого выполняется публикация (с учётом регистра), а сам репозиторий должен быть публичным. Если npm ci завершается ошибкой из-за конфликтов peer-зависимостей, см. статью How to Fix npm ERESOLVE Errors. О настройке 2FA и доверенной публикации читайте в статье npm Security Best Practices.
Заключение
Правила игнорирования в npm не выдают ошибок. Добавление одного файла незаметно меняет набор публикуемых файлов. Выберите либо files, либо .npmignore и удалите второй механизм, не размещайте ignore-файлы в подкаталогах и запускайте в CI npm pack --dry-run вместе с grep-проверкой на секреты перед каждым npm publish.
Часто задаваемые вопросы
Чем npm pack --dry-run отличается от npm publish --dry-run?
Обе команды показывают, какие файлы попадут в tarball, ничего не загружая, поэтому любая из них выявит ошибки в ignore-файлах. npm pack --dry-run — более простая проверка содержимого. npm publish --dry-run репетирует саму команду публикации. В актуальных версиях npm она также запрашивает у реестра, существует ли уже такая версия, и завершается ошибкой, если существует, но отсутствие авторизации вызывает лишь предупреждение. Успешный запуск не гарантирует, что загрузка пройдёт, поскольку авторизация и 2FA по-настоящему проверяются только в момент загрузки.
Как проверить, какие файлы содержит уже опубликованная версия?
Запустите npm pack с указанием версии из реестра, например npm pack my-lib@1.4.2. npm скачает опубликованный tarball, сохранит его в текущий каталог как my-lib-1.4.2.tgz и выведет имена файлов. Просмотреть содержимое можно командой tar -tzf my-lib-1.4.2.tgz. Чтобы получить URL tarball для curl или другого инструмента, выполните npm view my-lib@1.4.2 dist.tarball.
Мешает ли npm deprecate скачивать версию с утёкшим файлом?
Нет. npm deprecate добавляет к записи в реестре предупреждение, которое npm показывает всем, кто устанавливает эту версию, но tarball остаётся доступным для скачивания. Любой, кто зафиксировал точную версию, по-прежнему получит утёкший файл. Удалить версию из реестра можно только отменой публикации, а она ограничена политикой npm. Чтобы позже снять пометку deprecated, снова выполните npm deprecate с пустой строкой в качестве сообщения.
Можно ли использовать glob-шаблоны в поле files файла package.json?
Да. Можно указывать простые имена файлов, папки или glob-шаблоны, например * и **/*, а папка включается со всем содержимым. Синтаксис — это .gitignore наоборот: совпавший шаблон добавляет файл в tarball, а не исключает его. Файлы, которые npm включает всегда, — package.json, README, LICENSE и файл из main — публикуются независимо от того, указаны ли они в массиве.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k