Манифест 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).
Разделение важно по одной причине: 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 <имя>:
Пара важных моментов про 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 (и сборщикам), как именно загружать ваш пакет.
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-приложения в реальной жизни:
Обратите внимание на 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.