Menu
Русский

package.json: скрипты, зависимости и версии

Разбираем package.json по полочкам: какие поля реально важны, как работают scripts и что означают semver-диапазоны при установке пакетов.

Манифест Node-проекта

У каждого Node.js-проекта в корне лежит package.json. Это обычный JSON-файл, в котором описан сам проект — его имя, версия, зависимости, доступные команды — и именно его читает npm при любой операции. Удалишь его — и npm больше не понимает, с чем вообще имеет дело.

Быстрее всего создать его через npm init:

npm init -y

Флаг -y пропускает все вопросы и подставляет значения по умолчанию. В итоге получится что-то такое:

{
  "name": "my-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Это минимальный скелет. Большинство полей сами по себе ничего особенного не делают — они становятся полезными, когда вы начинаете добавлять зависимости и скрипты.

dependencies и devDependencies

Основную работу в package.json делают два поля: dependencies и devDependencies. Оба представляют собой объекты, где ключ — имя пакета, а значение — диапазон версий (semver).

index.js
Output
Click Run to see the output here.

Разделение важно по одной причине: dependencies — это пакеты, нужные вашему коду для работы. А devDependencies нужны только во время разработки: тестовые раннеры, линтеры, сборщики, проверка типов. Когда кто-то устанавливает ваш пакет как зависимость в свой проект, npm подтягивает ваши dependencies и игнорирует devDependencies.

Эти поля npm обновляет сам. Команда npm install express добавит строчку в dependencies, а npm install --save-dev vitest — в devDependencies. Руками их править приходится редко.

semver диапазоны: ^, ~ и точные версии

Строки вроде ^4.19.0 — это не конкретные версии, а диапазоны. npm работает по правилам semver, где версия делится на три части: MAJOR.MINOR.PATCH:

  • MAJOR — смена мажорной версии ломает обратную совместимость.
  • MINOR — добавляет новые возможности, ничего не ломая.
  • PATCH — исправляет баги.

Два оператора, которые встречаются чаще всего:

"express": "^4.19.0"   // >= 4.19.0 и < 5.0.0  (любая 4.x.x не ниже 4.19.0)
"express": "~4.19.0"   // >= 4.19.0 и < 4.20.0 (любая 4.19.x не ниже 4.19.0)
"express": "4.19.0"    // точно 4.19.0

^ — это значение по умолчанию, которое npm подставляет при установке пакета. Он разрешает обновления minor и patch, считая их совместимыми. ~ ведёт себя осторожнее — пропускает только патчи. А просто версия без префикса фиксирует её намертво.

Подвох в том, что «то, что я только что установил» и «то, что разрешает semver диапазон» — вещи разные. Вы сегодня ставите express@4.19.0, а коллега через месяц разворачивает проект — и у него ^4.19.0 уже резолвится в 4.19.5. Вот здесь на сцену выходит package-lock.json: он запоминает точные версии, до которых всё разрешилось, чтобы у всех в команде было одинаковое дерево зависимостей. Коммитьте его.

npm scripts: командный интерфейс вашего проекта

Поле scripts — это место, где вы описываете короткие алиасы для частых команд. Всё, что вы туда положите, запускается через npm run <имя>:

index.js
Output
Click Run to see the output here.

Пара важных моментов про npm scripts:

  • npm start, npm test и ещё несколько зарезервированных имён запускаются без слова run. Для всего остального нужен npm run <имя>.
  • Скрипты выполняются в shell, где node_modules/.bin уже прописан в PATH. Благодаря этому бинарники из установленных пакетов можно вызывать напрямую — "test": "vitest" работает, даже если vitest глобально не ставился.
  • Скрипты можно сцеплять: "build": "npm run lint && npm run compile". && означает «выполнять по очереди и падать при первой ошибке».
  • Скрипты вида pre<имя> и post<имя> запускаются автоматически. Если у вас есть prebuild, он сам отработает перед build — никакой дополнительной настройки не нужно.

Скрипты — это, по сути, панель управления проектом. Хороший package.json устроен так, что новому разработчику достаточно клонировать репозиторий, сделать npm install, а потом запустить npm run dev или npm test — и не лезть в вики.

Точки входа: main, exports, type

Эти поля подсказывают Node (и сборщикам), как именно загружать ваш пакет.

index.js
Output
Click Run to see the output here.
  • type определяет, как парсятся .js-файлы. Значение "module" включает ESM (import / export). Если поле опустить или поставить "commonjs", получите CommonJS (require). Подробности — в отдельной статье про CommonJS vs ESM.
  • main — это legacy-точка входа: то, что вернёт require("my-lib"). Старые инструменты всё ещё её учитывают.
  • exports — современная и более строгая замена. Здесь явно перечисляется, какие файлы пользователи вашего пакета вообще могут импортировать и по каким подпутям. Если файла в списке нет — импорт упадёт с ошибкой. Это не баг, а фича: вы сами контролируете публичный API.

Если вы просто пишете приложение, а не публикуете пакет, из всего этого вам реально пригодится только type.

Как выглядит package.json на практике

Соберём всё вместе. Вот примерно так выглядит package.json у небольшого Node-приложения в реальной жизни:

index.js
Output
Click Run to see the output here.

Обратите внимание на engines.node. Это поле носит рекомендательный характер — npm выдаст предупреждение (или ошибку, если включён engine-strict), когда версия Node у пользователя не совпадает. Хороший тон для всего, что вы публикуете.

Поля, о которых стоит знать

Ещё несколько полей, с которыми вы рано или поздно столкнётесь:

  • private: true — страховка от случайной публикации пакета в npm. Ставьте в любом проекте, который не предназначен для публикации.
  • license — SPDX-идентификатор вида "MIT" или "ISC". Важно для всего публичного.
  • repository, bugs, homepage — эти поля отображаются на странице пакета в реестре npm.
  • bin — если ваш пакет поставляет CLI, здесь вы сопоставляете имена команд со скриптами. После установки эти команды становятся доступны как исполняемые.
  • workspaces — для монорепозиториев; говорит npm рассматривать подкаталоги как связанные пакеты.

Не нужно заполнять всё подряд. Нужны только те поля, которые реально соответствуют вашей задаче.

Типичные ошибки

Вот на чём чаще всего спотыкаются:

  • Коммит node_modules в репозиторий. Не надо. Добавьте каталог в .gitignore. Связки package.json + package-lock.json достаточно, чтобы любой мог восстановить зависимости через npm install.
  • Не коммитить package-lock.json. А вот его как раз надо коммитить. Без lock-файла легко получить классическое «у меня работает»: semver-диапазоны со временем резолвятся в разные версии.
  • Рантайм-зависимости лежат в devDependencies. Локально всё работает, потому что dev-зависимости установлены, а в продакшене — падает, ведь там их пропускают. Если код использует пакет в рантайме, ему место в dependencies.
  • Править версии руками без переустановки. Поменяли версию в package.json — запустите npm install, иначе node_modules и lock-файл разойдутся.

Дальше: сам рантайм Node

package.json говорит Node, что представляет собой ваш проект. А рантайм Node решает, как его запускать — резолвинг модулей, встроенные модули, глобальные объекты, event loop под капотом. Об этом — на следующей странице.

Часто задаваемые вопросы

Для чего нужен package.json?

Это манифест Node.js-проекта. В нём хранятся имя и версия пакета, список зависимостей, команды, которые можно запускать через npm run, а также служебные поля — точка входа, тип модуля и прочее. Именно этот файл читает npm install, чтобы понять, что вообще нужно скачать.

Чем dependencies отличается от devDependencies?

В dependencies лежит то, без чего код не заработает в проде — например, express или react. В devDependencies — всё, что нужно только во время разработки и сборки: тест-раннеры, бандлеры, линтеры. Когда кто-то ставит ваш пакет как зависимость своего проекта, npm devDependencies пропустит.

Что значат символы ^ и ~ в версиях package.json?

Это операторы semver-диапазонов. ^1.2.3 разрешает любую версию 1.x.x не ниже 1.2.3 (та же мажорная). ~1.2.3 жёстче — пустит только 1.2.x начиная с 1.2.3 (та же минорная). Просто 1.2.3 без префикса фиксирует точную версию. А конкретные установленные версии записываются в package-lock.json — чтобы установка у всех воспроизводилась одинаково.

Как создать файл package.json?

Перейдите в пустую папку и выполните npm init — команда задаст несколько вопросов. Если отвечать лень, запустите npm init -y и получите файл со значениями по умолчанию. Можно и руками написать — это обычный JSON. Обязательных полей по сути два: name и version.

Учитесь программировать с Coddy

НАЧАТЬ