Версионирование — semver и lock-файлы

Зачем нужно

Без чёткой системы версий обновление любой библиотеки может сломать проект. Semver (Semantic Versioning) задаёт правила: какое обновление безопасно, а какое содержит несовместимые изменения. Lock-файлы гарантируют что у всех разработчиков и на CI/CD стоят идентичные версии пакетов.

Где используется

  • Управление зависимостями в package.json (^, ~, exact)
  • Публикация npm-пакетов (когда и как менять версию)
  • CI/CD: npm ci для воспроизводимых сборок
  • Обновление зависимостей без поломки проекта
  • Понимание changelog-ов библиотек

Предпосылки


Semantic Versioning (semver)

MAJOR.MINOR.PATCH
  │      │     │
  │      │     └── Баг-фикс (обратно совместимый)
  │      └──────── Новая функциональность (обратно совместимая)
  └─────────────── Несовместимое изменение API (breaking change)

Примеры:
  1.0.0 → 1.0.1   Patch: исправлена ошибка
  1.0.0 → 1.1.0   Minor: добавлена новая функция
  1.0.0 → 2.0.0   Major: изменён/удалён существующий API

Примеры из реальных библиотек

Express 4.18.2 → 4.18.3  (patch)
  Исправлена уязвимость в парсере URL

Express 4.18.2 → 4.19.0  (minor)
  Добавлен новый middleware, старый код работает

Express 4.x → 5.0.0  (major)
  Удалён app.del, изменена обработка ошибок
  Старый код МОЖЕТ сломаться

0.x.x — версия до "стабильного" релиза
  Любое изменение может быть несовместимым
  0.1.0 → 0.2.0 может быть breaking change

Диапазоны версий в package.json

^ (caret) — совместимые обновления

{
  "dependencies": {
    "express": "^4.18.2"
  }
}
^4.18.2 означает: >=4.18.2 и <5.0.0

  ✅ 4.18.3  (patch)
  ✅ 4.19.0  (minor)
  ✅ 4.99.99
  ❌ 5.0.0   (major — исключается)

^ — дефолт npm install. Самый распространённый.
Логика: "обновляй, но не ломай мне API"

~ (tilde) — только patch-обновления

{
  "dependencies": {
    "lodash": "~4.17.21"
  }
}
~4.17.21 означает: >=4.17.21 и <4.18.0

  ✅ 4.17.22  (patch)
  ✅ 4.17.99
  ❌ 4.18.0   (minor — исключается)
  ❌ 5.0.0    (major — исключается)

~ — более консервативный. Только баг-фиксы.

Точная версия (exact)

{
  "dependencies": {
    "react": "18.2.0"
  }
}
18.2.0 означает: ТОЛЬКО 18.2.0

  ❌ 18.2.1
  ❌ 18.3.0
  ❌ 19.0.0

Используется когда нужна полная воспроизводимость
или когда minor/patch обновления ломали проект

Другие диапазоны

{
  "dependencies": {
    "a": ">=4.0.0",
    "b": ">=4.0.0 <5.0.0",
    "c": "4.x",
    "d": "4.18.x",
    "e": "*",
    "f": "latest"
  }
}
>=4.0.0          — любая от 4.0.0 и выше
>=4.0.0 <5.0.0   — от 4.0.0 до 5.0.0 (не включая)
4.x              — любая 4.x.x (= ^4.0.0)
4.18.x           — любая 4.18.x (= ~4.18.0)
*                — любая версия (опасно!)
latest           — последняя (= *)

Сравнение

                  4.17.21  4.17.22  4.18.0  5.0.0
──────────────────────────────────────────────────
^4.17.21             ✅      ✅      ✅     ❌
~4.17.21             ✅      ✅      ❌     ❌
4.17.21              ✅      ❌      ❌     ❌
>=4.17.21 <5.0.0     ✅      ✅      ✅     ❌
*                    ✅      ✅      ✅     ✅

package-lock.json()

Зачем нужен

Проблема без lock-файла:

  package.json: "express": "^4.18.2"

  Понедельник: npm install → express@4.18.2
  Среда:       npm install → express@4.18.3 (вышел patch)
  Пятница:     npm install → express@4.19.0 (вышел minor)

  Разные версии у разных разработчиков и на CI!

Решение — package-lock.json():
  Фиксирует ТОЧНЫЕ версии ВСЕХ зависимостей (включая транзитивные)
  npm install читает lock-файл и ставит точно те же версии

Структура

{
  "name": "my-app",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "packages": {
    "": {
      "name": "my-app",
      "version": "1.0.0",
      "dependencies": {
        "express": "^4.18.2"
      }
    },
    "node_modules/express": {
      "version": "4.18.2",
      "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
      "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMg==",
      "dependencies": {
        "accepts": "~1.3.8",
        "body-parser": "1.20.1"
      }
    }
  }
}
resolved  — откуда скачан пакет (URL)
integrity — хеш для проверки целостности (SHA512)

Lock-файл фиксирует:
  - Точную версию каждого пакета (включая зависимости зависимостей)
  - URL, откуда скачан
  - Хеш для проверки

ВСЕГДА коммитьте package-lock.json() в git!

npm ci vs npm install

# npm install:
#   - Читает package.json
#   - Обновляет package-lock.json() если нужно
#   - Ставит пакеты в рамках semver-диапазонов
#   - Для разработки

# npm ci (Clean Install):
#   - Читает ТОЛЬКО package-lock.json()
#   - Удаляет node_modules перед установкой
#   - НЕ модифицирует package-lock.json()
#   - Ошибка если lock-файл не соответствует package.json
#   - Для CI/CD (воспроизводимые сборки)

# Разработка
npm install

# CI/CD, Docker
npm ci

Когда использовать что

npm install:
  ✅ Локальная разработка
  ✅ Добавление новых пакетов
  ✅ Обновление зависимостей

npm ci:
  ✅ CI/CD пайплайны
  ✅ Docker-сборки
  ✅ Production-деплой
  ✅ Когда нужна 100% воспроизводимость
# Dockerfile — пример
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json() ./
RUN npm ci --production         # ← npm ci, не npm install
COPY . .
CMD ["node", "dist/index.js"]

npm shrinkwrap

# npm shrinkwrap — создаёт npm-shrinkwrap.json()
# Аналог package-lock.json(), но:
#   - Публикуется в npm registry
#   - Используется при npm install вашего пакета

npm shrinkwrap

# Применение: когда вы ПУБЛИКУЕТЕ пакет и хотите
# зафиксировать точные версии зависимостей для пользователей
# (редко нужно, обычно достаточно package-lock.json())

Обновление версий

Для своего пакета

# npm version автоматически:
# 1. Меняет version в package.json
# 2. Создаёт git commit
# 3. Создаёт git tag

npm version patch   # 1.0.0 → 1.0.1
npm version minor   # 1.0.0 → 1.1.0
npm version major   # 1.0.0 → 2.0.0

# С префиксом pre-release
npm version prepatch   # 1.0.0 → 1.0.1-0
npm version preminor   # 1.0.0 → 1.1.0-0
npm version premajor   # 1.0.0 → 2.0.0-0

# Конкретная версия
npm version 3.0.0-beta.1

Для зависимостей

# Обновить в рамках semver-диапазонов
npm update

# Посмотреть устаревшие пакеты
npm outdated
# Package   Current  Wanted  Latest
# express    4.18.2  4.18.3  5.0.0
# lodash    4.17.20 4.17.21 4.17.21

# "Wanted" — максимум в рамках ^/~
# "Latest" — самая новая версия

# Обновить конкретный пакет до latest (выходя за semver)
npm install express@latest

# Интерактивное обновление (npx)
npx npm-check-updates -i

Breaking Changes — как справляться

Когда зависимость выпускает major-версию:

1. Прочитать CHANGELOG / Migration Guide
2. Создать ветку: git checkout -b update-express-v5
3. Обновить: npm install express@5
4. Запустить тесты: npm test
5. Исправить несовместимости
6. Проверить в staging
7. Влить в main

Никогда не обновляй major-версии "вслепую"!

Pre-release версии

Версии с метками:
  1.0.0-alpha.1    — альфа (ранняя, нестабильная)
  1.0.0-beta.1     — бета (feature-complete, но с багами)
  1.0.0-rc.1       — release candidate (почти готово)
  1.0.0             — стабильный релиз

Порядок сортировки:
  1.0.0-alpha.1 < 1.0.0-beta.1 < 1.0.0-rc.1 < 1.0.0

Установка pre-release:
  npm install react@next
  npm install express@5.0.0-beta.1

Частые ошибки

  1. Не коммитят package-lock.json() — разные версии у разных разработчиков, сборка непредсказуема
  2. Используют npm install в CI — вместо npm ci, lock-файл может обновиться на CI
  3. * или latest в dependencies — любое обновление может сломать проект
  4. Не читают changelog при major-обновлении — код ломается из-за breaking changes
  5. Путают ^ и ~^ допускает minor-обновления (новые фичи), ~ только patch (баг-фиксы)
  6. Удаляют package-lock.json() при проблемах — вместо npm ci или анализа конфликтов

Практика

  1. Установить пакет с ^, ~ и точной версией, сравнить package.json
  2. Запустить npm outdated и разобрать Wanted vs Latest
  3. Использовать npm ci — убедиться что он удаляет node_modules и ставит из lock-файла
  4. Выполнить npm version patch и проверить git log (commit + tag)
  5. Обновить зависимость до следующего major-релиза, исправить несовместимости

Связанные темы

  • npm basics — команды npm install, update, audit
  • package.json — dependencies, devDependencies, engines
  • npx — запуск конкретных версий

Ресурсы


🎓 Источник: Настройка среды Node.js, npm, git, eslint

  • 📅 2018-10-01 · YouTube
  • Тезисы:
    • Маска ^4.18.0 — совместимый минор (4.x.x, но не 5.0)
    • ~4.18.0 — только патчи (4.18.x)
    • Точная версия 4.18.0 — фиксирует, но lock-файл нужен всё равно для транзитивных
    • package-lock.json() хранит SHA-512 каждой зависимости — целостность гарантирована
    • В production коммитят lock-файл, иначе сборки не воспроизводимы
  • Цитата: «Пишите только шляпку (^) — патчи и миноры безопасны, мажор может ломать»

🎓 Источник: Летняя школа 2017 — Настройка среды

  • 📅 2019-11-14 · YouTube
  • Тезисы: semver: шляпка и тильда, package-lock хранит точные версии и хэши, раньше коммитили node_modules целиком