Как стать автором
Поиск
Написать публикацию
Обновить

Как собрать npm-пакет в 2025 и не облажаться

Уровень сложностиСредний
Время на прочтение6 мин
Количество просмотров479

Наши кодовые базы растут, и вынос кода в npm-пакеты — один из самых простых и рабочих способов держать этот рост под контролем. Фронтендеры уже освоились со сборкой приложений — мы минифицируем и бандлим код для ускорения загрузки, подключаем полифиллы и транспилируем для поддержки старых браузеров. Есть соблазн для библиотек просто делать все то же самое — но это ошибка, потому что у библиотек совсем другие ограничения. Вот мой топ (нефункциональных) ценностей библиотеки:

  1. Работает на всех целевых платформах.

  2. Подключается без специальных приседаний.

  3. Не мешает оптимизировать бандл.

  4. Не кладет лишнего в node_modules.

  5. Дебажится и ремонтируется в домашних условиях.

В прошлой статье мы разобрались с самым холиварным вопросом — в 2025 году библиотеки уже можно публиковать в esm-only формате. Сегодня закончим тему сборки и выясним, нужны ли библиотекам:

  1. Минификация

  2. Транспиляция

  3. Полифиллы

  4. Сорсмапы

  5. src прямо на npm

  6. Бандлинг

Сегодня поговорим только про традиционное использование пакетов на node или через бандлер, сборку для CDN не трогаем. Поехали!

Минификация

Начнем с простого — стоит ли минифицировать код, который мы публикуем? Мы минифицируем код клиентской части приложений, чтобы грузить меньше данных по сети. Применимо ли это к библиотекам?

На размер итогового бандла приложения и скорость сборки такая пред-минификация не влияет. При сборке конечного проекта все библиотеки в любом случае пройдут минификацию. Сильно ускорить или улучшить этот процесс вы не сможете, потому что а) бандлер не знает, что эти файлы уже маленькие, и б) ваш код все равно надо дооптимизировать под конкретное использование (потришейкать / заинтайлить).

Есть ли еще аргументы? Да, с минифицированным кодом node_modules будут меньше, а установка быстрее. Но если у пользователя что-то идет не так, дебажить и чинить библиотеку гораздо сложнее (кто хоть раз ковырял обфусцированный код, тот знает).

Значит, минифицировать библиотеки не стоит. Зачем некоторые сборщики библиотек по умолчанию минифицируют код — для меня загадка.

Транспиляция

Транспиляция может значить несколько вещей: и компиляция compile-to-JS языков, и пересборка в более старую версию ES, например a?.b -> a == null ? undefined : a.b. Ответы для этих случаев будут немного отличаться.

Да, нам нужно сделать нормальный JS из нестандартного JS-синтаксиса — JSX, TS, Vue SFC, не дай бог ReScript или на чем там пишут детишки. Без этого наш код не заведется в node (хотя для TS это может скоро измениться), а бандлер придется специально настраивать, чтобы пересобрать библиотеку для браузера.

Более интересный вопрос — нужно ли транспилировать код в пониженную версии ES для более старых рантаймов. На сервере или локально (node / deno / bun) код с неподдерживаемым синтаксисом взорвется. Для браузера код пройдет через бандлер, но оттранспилируется ли он? В vite — да, весь код пройдет через esbuild. А для webpack базовый babel-loader явно рекомендуют для node_modules отключить. То есть возможность до-транспилировать код для браузера есть, но ее часто придется настраивать вручную.

Итого: библиотекам все таки нужно явно продумывать поддержку node и браузеров и транспилироваться под них. Но транспиляция — деструктивный процесс: пересобранная версия может раздувать бандл и хуже перформить в рантайме, а провернуть этот фарш назад уже не выйдет. Так что оптимально — выбрать достаточно современный таргет (node20 / chrome112), указать его в документации и в package.json поле engines. Если пользователи хотят поддерживать более старые браузеры — включат транспиляцию нашей библиотеки.

Полифиллы

Где транспиляция, там и полифиллы. Напомню, транспиляция — изменение синтаксиса, полифилл — добавление встроенных объектов или их методов. Например, в ES2025 есть Set.prototype.union, и наконец-то можно объединять множества.

Ситуация с полифиллами для библиотек печальная: они бы нам очень пригодились (методы в JS добавляют чаще, чем синтаксис), но использовать их мы не можем. Классический полифилл — это глобальный сайдэффект, который манкипатчит глобальные объекты (Set.prototype.union = Set.prototype.union || customUnion). Это создает проблемы:

  1. Если наши пользователи используют другой полифилл, применится только тот, который подключили первым.

  2. Добавить полифилл легко, а убрать ненужный — сложно.

  3. Сайдэффекты мешают тришейкать код, потому что бандлер должен сохранить порядок всех модулей с сайдэффектами.

  4. Это дополнительная зависимость библиотеки, которую надо иногда обновлять.

Отличных решений нет, предложу 2 варианта:

  1. Руками использовать только объекты и методы, доступные в выбранных рантаймах. В этом помогут опция lib в tsconfig.json и юнит-тесты на целевых версиях рантаймов.

  2. Явно задокументировать, какие не-общедоступные методы нужны библиотеке (но помнить, что документацию никто не читает).

Сорсмапы

Вспомним, что основная цель сорсмапов — читаемые стектрейсы в сильно преобразованном коде. Нужно ли это npm-пакетам?

Если мы минифицировали код и пытаетемся скомпенсировать это через сорсмапы — мы проиграли, потому что сорсмап содержит весь исходный код. Общий publish size вырастет и лишит нас единственного преимущества минификации.

Если код библиотеки попадает в браузер через бандлер, то наши сорсмапы не смержатся с сорсмапами приложения без приседаний вроде rollup-plugin-sourcemaps2.

Единственный кейс, в котором можно подумать про сорсмапы — библиотеки не на чистом JS / TS (например, JSX / Vue SFC). При этом помним, что из коробки эти сорсмапы применятся только в node.

Публиковать ли src?

Исходный код публиковать на npm не надо, если репозиторий доступен всем на GitHub (опенсорс) или на нашем рабочем гите, а эти пункты всегда должны выполняться.

Давайте пробежимся по возможным кейсам, где пользователям может понадобится src:

  1. Посмотреть: если код не минифицирован и не сильно транспилирован, можно совершенно так же посмотреть на собранную версию. Если всё-таки сильно транспилирован, то на гитхабе.

  2. Поредактировать, пересобрать и проверить на своем проекте: гораздо удобнее склонировать репу, поработать с ней и подключить к себе через npm link.

  3. Подключить несобранную версию библиотеки и пересобрать самостоятельно как часть приложения. Это нездоровое желание, которое скорее всего говорит о том что официальная сборка сломана. Впрочем, если очень надо — клонируем репу в src/npm-lib и ставим через npm i src/npm-lib.

src — мусор в node_modules. Не публикуйте src.

Бандлинг

Самый холиварный вопрос — нужно ли бандлить библиотеки? Начнем с преимуществ, которые это может дать:

  1. Ускорить сборку конечного приложения: бандлеру не нужно бандлить код библиотеки.

  2. Ускорить dev-режим — на vite это не повлияет из-за dependency pre-bundling, для webpack может быть какой-то профит.

  3. Ускорить импорт библиотеки в node — но он и так быстрый, например, 247 модулей из date-fns импортируются у меня за 0.6мс, трудно представить когда это будет критично.

  4. Раньше бандлинг помогал инкапсулировать API библиотеки, потому что можно было напрямую импортировать что-нибудь приватное через import { secretInternal } from 'lib/dist/secretInternal'. Сейчас проблема решается через exports в package.json

Теперь к минусам:

  1. Для браузерных библиотек бандлинг может помешать разбить код библиотеки на несколько чанков (в общем случае деление одного js-модуля на несколько — небезопасная операция). Пример: в приложении есть import { core } from 'lib' на всех страницах и import { core, heavyExtra } from 'lib' в одном месте. Если core и heavyExtra живут в одном js-файле, они скорее всего будут всегда грузиться вместе, а если в разных — есть хороший шанс грузить heavyExtra только когда он правда нужен.

  2. Усложняет ремонт приложения, потому что редактировать один огромный файл сложнее, чем несколько маленьких.

  3. Дополнительный шаг сборки библиотеки, но с этим можно смириться, если мы сэкономим время всем своим пользователям.

Скажу честно: ни плюсы, ни минусы бандлинга не кажутся мне очень убедительными. Единственное, в чем я уверен — ощутимая разница будет только а) для времени обработки бандлером, и б) только для очень больших библиотек. Отдельный вопрос — стоит ли бандлить .d.ts файлы. Вполне допускаю, что это полезно, но пока не готов глубоко рисерчить эту тему. В другой раз.

А, чуть не забыл: главное правило бандлинга библиотек — не вбандливайте зависимости. Представим: мы используем date-fns в библиотеке, наш пользователь использует date-fns в своем приложении. Если date-fns вбандлить в нашу библиотеку, к конечным пользователям поедет два date-fns. Нехорошо. Касается и обычных dependencies, и (особенно) peerDependencies.


Итого, как собирать библиотеки в 2025:

  1. Не минифицируем: это усложняет дебаг библиотеки и не дает никакого профита.

  2. Не публикуем сорсмапы: они не работают в бандлере и раздувают пакет.

  3. Не используем полифиллы: они завязывают ваш код на глобальные сайд-эффекты и могут конфликтовать с полифиллами наших пользователей.

  4. Не публикуем исходный код на npm: он и так доступен в нашем репозитории.

  5. Транспилируем в чистый JS: не стоит заставлять пользователей страдать с настройкой бандлера.

  6. Явно выбираем таргеты поддержки node и браузеров, транспилируем синтаксис для них (но не пытаемся поддержать самое старьё, это вредит большинству пользователей).

  7. Не бандлим, пока это не тормозит сборку конечных приложений. Никогда не бандлим зависимости.

Уф, вроде разобрались. Остаюсь на связи, в следующий раз поговорим, какие инструменты стоит использовать для сборки библиотек.

Теги:
Хабы:
+5
Комментарии3

Публикации

Ближайшие события