npm и сторонние пакеты

модули≈ 9 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Почти любой проект на JavaScript состоит не только из вашего кода: заметную часть работы делают чужие пакеты, установленные командой npm install. Разберёмся, откуда они берутся, где лежат, что означают значки вроде ^1.4.2 — и почему выбирать пакет стоит внимательнее, чем кажется.

Разделы темы:·

В корне проекта лежит файл package.json — обычный JSON, который npm читает перед каждой командой. Там записано, как проект называется, какая у него версия, какие пакеты ему нужны и какие команды он умеет запускать. Создаётся файл командой npm init -y, а дальше растёт сам по мере установки пакетов.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Отдельного внимания стоит поле scripts — словарь коротких имён для длинных команд. Написали npm run test, npm нашёл строку в словаре и выполнил её в терминале. Это способ не держать в голове флаги сборщика и не пересылать их коллегам в чат.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Команда npm install dayjs выглядит мгновенной, но делает сразу несколько вещей: спрашивает у реестра registry.npmjs.org, где лежит пакет с таким именем, скачивает архив, распаковывает его в папку node_modules, дописывает строчку в dependencies и запоминает точную скачанную версию в файле package-lock.json.

Дальше имя пакета работает как адрес: свой файл вы подключаете путём — ./utils.js, а пакет просто именем — import dayjs from "dayjs". Внутри пакет устроен ровно так же, как ваши модули: файл, который что-то экспортирует. Никакой магии, просто чужой код рядом с вашим.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Имя вместо пути понимают Node и сборщики: они знают, что пакет надо искать в node_modules. Браузер про эту папку ничего не знает, ему нужен адрес файла, а голое имя "dayjs" адресом не является. Примеры учебника выполняет сам браузер, без всякого сборщика, так что проверить можно прямо здесь. Прогноз: загрузится ли пакет?

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Не загрузилось: браузер не смог превратить имя в адрес и отклонил промис с TypeError. Адрес имени даёт карта импортов — тег <script type="importmap"> со словарём в формате JSON: слева имя, справа адрес. На странице следующего примера такая карта уже стоит, и под именем dayjs в ней не файл, а крошечный модуль, записанный прямо в адресе: data:text/javascript,export default …. Прогноз: что напечатает пример?

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Имя нашлось в карте, и модуль загрузился так, будто это настоящий пакет. Карты импортов понимают все основные браузеры: последним их поддержал Safari, в марте 2023 года. На настоящей странице карта указывает на файл рядом со страницей, а точку входа подключают, как в статье о модулях, тегом с type="module":

shлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.
вывод · терминал, curl 8.13.0; страница с локального сервера в Chrome 152 вывела в консоль «15.09.2026»; macOS arm64, 15.09.2026
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<script type="importmap">
{ "imports": { "dayjs": "./vendor/dayjs.js" } }
</script>
<script type="module" src="./main.js"></script>
</head>
<body></body>
</html>
$ cat main.js
import dayjs from "dayjs";
console.log(dayjs("2026-09-15").format("DD.MM.YYYY"));
$ curl -sL --create-dirs -o vendor/dayjs.js https://cdn.jsdelivr.net/npm/dayjs@1.11.23/+esm
$ head -3 vendor/dayjs.js
/**
* Bundled by jsDelivr using Rollup v4.62.2 and esbuild v0.28.1.
* Original file: /npm/dayjs@1.11.23/dayjs.min.js

Файл в vendor скачан с jsDelivr — сервиса, который раздаёт файлы пакетов npm, а по адресу с +esm отдаёт пакет ES-модулем; первые строки файла сообщают, что для этого jsDelivr пропустил пакет через сборщик. Главный файл dayjs из node_modules на это место не встанет: он написан для require и обычного <script>, export в нём нет, и импорт в main.js упадёт с SyntaxError.

В этом и граница карты: она только даёт имени адрес, а файл по адресу браузер выполняет как есть. Карты хватает, пока в проекте нет ни JSX и TypeScript, ни пакетов на CommonJS, ни пакетов из сотен файлов, ни хеша в именах файлов ради долгого кеша. С первым же из этих пунктов нужен сборщик, и что именно он делает, разбирает статья «Свой сборщик» в части IV.

КомандаЧто делает
npm init -yсоздаёт заготовку package.json
npm installставит всё, что перечислено в package.json
npm install dayjsскачивает пакет и дописывает его в dependencies
npm install -D vitestто же самое, но в devDependencies
npm uninstall dayjsудаляет пакет и строку из package.json
npm ciставит строго по package-lock.json, начисто

Это другие клиенты к тому же самому реестру: пакеты те же, отличаются скорость, раскладка node_modules и имя lock-файла. Важное правило — в одном проекте живёт один менеджер. Если рядом окажутся package-lock.json и pnpm-lock.yaml, у разных участников команды соберутся разные версии, и вечер перестанет быть томным.

В dependencies попадает то, без чего приложение не работает у пользователя: библиотека дат, валидатор, сам фреймворк. В devDependencies — инструменты, нужные только вам за компьютером: сборщик, линтер, тестовый фреймворк, типы для TypeScript. Флаг -D (он же --save-dev) как раз кладёт пакет во второй список.

Разница становится заметной на сервере: npm ci --omit=dev ставит только боевые пакеты, и образ приложения худеет в разы. Ошибка «положил линтер в dependencies» ничего не сломает сразу — она просто утяжелит сборку и добавит лишнего чужого кода туда, где он никому не нужен.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Версия пакета — три числа через точку: 2.4.1. Это соглашение называется семантическим версионированием, semver. Последнее число меняют, когда починили ошибку; среднее — когда добавили новое, но старое продолжает работать; первое — когда что-то сломали намеренно, и обновление потребует правок в вашем коде.

Поэтому в package.json хранится не версия, а диапазон. Значок ^, который npm ставит по умолчанию, означает «бери свежее, но не переходи через мажорную границу».

ЗаписьКакие версии подойдутКогда так пишут
^1.4.2от 1.4.2 до 2.0.0, не включая еёпо умолчанию, в большинстве случаев
~1.4.2от 1.4.2 до 1.5.0, не включая еёнужны только исправления
1.4.2ровно эта, и никакая другаяпакет обновляется болезненно
*любаяпочти никогда
js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

node_modules — это склад, куда npm распаковывает скачанное. Его не коммитят в git: папка целиком восстанавливается из package.json и package-lock.json, поэтому её имя одним из первых попадает в .gitignore.

Главный сюрприз новичка — размер. Вы поставили пять пакетов, а внутри тысячи папок. Дело в том, что у каждого пакета есть свои зависимости, у тех — свои, и так вглубь. Соберём такое дерево вручную и посчитаем, сколько пакетов на самом деле приедет.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Если бы каждый пакет держал свои зависимости внутри себя, одна и та же библиотека скачалась бы десятки раз, а пути стали бы длиннее, чем терпит файловая система. Поэтому npm поднимает зависимости наверх, в общий плоский список, и кладёт вложенно только те, чьи версии конфликтуют. Менеджер pnpm идёт дальше: хранит все пакеты в одном месте на диске, а в проект кладёт ссылки — оттуда его экономность.

Устанавливая пакет, вы пускаете чужой код в проект с теми же правами, что и у собственного. История помнит показательные случаи: автор удалил из реестра крошечный left-pad — и сборки посыпались по всему миру; в популярный event-stream новый сопровождающий добавил код, охотившийся на криптокошельки. Это не повод бояться npm, это повод смотреть, что ставишь.

Перед установкой полезно потратить минуту на страницу пакета: когда был последний релиз, сколько своих зависимостей он тянет, отвечают ли на issues, есть ли типы для TypeScript, понятна ли лицензия. И самый честный вопрос — точно ли задача не решается десятком своих строк. В следующей статье, «Живой ли пакет», эта минута разложена на шесть вопросов вместо привычных звёзд и загрузок.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Каждая зависимость — обещание жить с ней годами: обновлять, чинить, объяснять новичкам. Иногда десять своих строк дешевле.

  • package.json — паспорт проекта: имя, версия, скрипты и списки зависимостей. npm читает его перед каждой командой.
  • npm install скачивает пакет в node_modules, дописывает строку в package.json и фиксирует точную версию в package-lock.json — lock-файл коммитят.
  • dependencies нужны приложению у пользователя, devDependencies — только вам во время разработки; на сервере вторые можно не ставить.
  • Версия — это три числа: мажор, минор, патч. Запись ^1.4.2 разрешает обновления до 2.0.0, ~1.4.2 — только патчи.
  • node_modules не коммитят: он восстанавливается из package.json и lock-файла. Каждый пакет тянет свои зависимости, поэтому ставьте осознанно.

1/3Куда попадёт пакет после команды npm install --save-dev eslint?

2/3Какие версии разрешает запись "^1.4.2"?

3/3Нужно ли коммитить node_modules в репозиторий?

Прочитать package.json

Дан уже разобранный package.json. Выведите имя проекта и общее количество зависимостей — боевых и разработческих вместе.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Списки зависимостей — обычные объекты, поэтому считаем их ключи через Object.keys:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Если поля devDependencies в файле нет, обращение к нему даст undefined и Object.keys упадёт. Подстраховка — Object.keys(pkg.devDependencies ?? {}).

Проверка диапазона ~1.2.3

Запись ~1.2.3 разрешает только патч-обновления: подойдут 1.2.3 и 1.2.9, но не 1.3.0 и тем более не 2.0.0. Напишите функцию, которая это проверяет.

js
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.

Разбираем строку на три числа и требуем совпадения мажорной и минорной части, а патч — не меньше исходного:

jsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Обратите внимание на map(Number): без него сравнение шло бы по строкам, и «10» оказалось бы меньше «9».