12k
All articles

Как не опубликовать в npm не те файлы

Не допускайте публикации лишних файлов и секретов в npm. Разберитесь, как взаимодействуют .npmignore, .gitignore и files в package.json, и проверьте архив командой npm pack --dry-run.

OpenReplay Team
OpenReplay Team
Как не опубликовать в npm не те файлы

Если в пакете есть файл .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

Что делать, если не те файлы уже опубликованы

Если секрет попал в реестр, первым делом замените его и считайте его навсегда публичным. Отмена публикации версии не удаляет копии, которые уже были скачаны или зеркалированы. После этого наведите порядок в реестре:

  1. Замените (ротируйте) учётные данные у их поставщика.
  2. Удалите неудачную версию или пометьте её. Согласно политике отмены публикации npm, отменить публикацию можно в течение 72 часов после неё, если от этой версии не зависит ни один другой публичный пакет. По истечении этого срока отменить публикацию можно только для пакетов, соответствующих критериям политики. В остальных случаях используйте npm deprecate.
  3. Выпустите чистый патч. Использованный номер версии нельзя использовать повторно, даже после отмены публикации.
# 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 — публикуются независимо от того, указаны ли они в массиве.

DevTools for the frontend

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

We use cookies to improve your experience. By using our site, you accept cookies.