npmパッケージのバージョン管理


バージョン管理の理解が乏しかったため、一度きちんと学習します。

# package.json
`dependencies`以外の内容は省略します。

例えば`express`と`serverless-express`を使用するアプリケーションを開発しているとして、
`npm install express serverless-express`で以下のような package.json が生成されます。
```package.json
{
  "name": "npm-test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "directories": {
    "test": "test"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.18.2",
    "serverless-express": "^2.0.12"
  }
}
```
ここに記載される`dependencies`は「**installする予定のパッケージ群**」を記載したものです。

### セマンティック バージョニング

`"4.18.2"` というバージョン表記は「**セマンティック バージョニング**」というルールに基づいています。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3537959/b252298c-e42c-ec67-91f4-43cc74016f48.png" width=40%>

バージョン更新は以下の規定に従い更新するというルールになっています。

| バージョン更新 | 変更内容                      |
|:------------:|:----------------------------|
| **major**    | 後方互換性のないAPIの変更/追加  |
| **minor**    | 後方互換性のあるAPIの変更/追加  |
| **patch**    | APIに影響がない bug-fix       |

なので major バージョンの更新でなければ、**基本は**開発しているアプリケーションへの影響は出ないと考えて良さそうです。

:::note warn
「基本は」そう考えていいと思いますが、
・そもそもnpmはオープンソースのリポジトリでバグがあるバージョンも普通に存在しうる
・バージョン更新によって依存パッケージも最新化される
という懸念点は存在するので、更新内容によってはプログラムがうまく動かなくなるという可能性もありそうです。
そのためパッケージのバージョン更新時は major バージョンの更新ではなくとも**テストは必要**、という考え方が自然かと思います。
:::


### バージョンの指定方法

npm install 時、デフォルトでは`^(キャレット)`によるバージョン指定でlatestのバージョンがpackage.jsonに記載されます。
`^(キャレット)`以外にも指定方法があり、以下の通りです。
| 指定方法 | 意味 | 例 |
|:-:|:-:|:-:|
| ^(キャレット)  |  左端のゼロ以外の数字を変更しない変更を許可 | ^1.2.3 := >=1.2.3 < 2.0.0 <br />  ^0.2.3:= >=0.2.3 < 0.3.0 |
| ~(チルダ)  | 右端以下のバージョンがあがることを許可 | ~1.2.3 := >=1.2.3 <1.3.0 <br /> ~1 := >=1.0.0 <2.0.0 |
| -(ハイフン)  | 指定した範囲を許可 <br/> 欠落部分はゼロに置き換えられる  | 1.2.3 - 2.3.4 := >=1.2.3 <=2.3.4 <br /> 1.2 - 2.3.4 := >=1.2.0 <=2.3.4 |
| X, x, *  | バージョンの数値の１つを「代用」  | * := >=0.0.0 <br/> 1.X := >=1.0.0 <2.0.0 <br/> 1.2.x := >=1.2.0 <1.3.0 |
| 固定 | 指定するバージョンを固定  | 1.2.3 |

:::note warn
上記の通り厳密には
・^(キャレット) = マイナーバージョン指定
・~(チルダ) = パッチバージョン指定
という意味ではない点に注意。
:::
（`>=1.2.3 || <2.0.0` のような指定にした方が混乱しなくないか？とも思いましたが、少し見にくくなってしまうので微妙ですかね ）


# package-lock.json

実際にinstallされたパッケージが`"packages"`に記載されます。

```json
{
  "name": "npm-test",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "npm-test",
      "version": "1.0.0",
      "license": "ISC",
      "dependencies": {
        "express": "^4.18.2",
        "serverless-express": "^2.0.12"
      }
    },
    "node_modules/@vendia/serverless-express": {
      "version": "3.4.0",
      "resolved": "https://registry.npmjs.org/@vendia/serverless-express/-/serverless-express-3.4.0.tgz",
      "integrity": "sha512-/UAAbi9qRjUtjRISt5MJ1sfhtrHb26hqQ0nvE5qhMLsAdR5H7ErBwPD8Q/v2OENKm0iWsGwErIZEg7ebUeFDjQ==",
      "dependencies": {
        "binary-case": "^1.0.0",
        "type-is": "^1.6.16"
      },
      "engines": {
        "node": ">=6"
      }
    },

{ "-- (中略) --" }

}
```

上の例で言うと
```json
"node_modules/@vendia/serverless-express": {
   "version": "3.4.0",
```
ここには ^ や ~ 指定がありません。
npm install をして `version 3.4.0` のパッケージがinstallされたという結果が記載されています。

なので、package.json の dependencies が「予定」であったのに対し、package-lock.json は「**パッケージをinstallした結果**」を記載したファイルになります。

また、package-lock.json にも^指定された`dependencies`の情報が記載されています。
```json
"dependencies": {
   "binary-case": "^1.0.0",
   "type-is": "^1.6.16"
}
```
これは `"node_modules/@vendia/serverless-express"`の package.json の dependencies に記載されている内容です。

そして、以下のように実際にinstallされた`binary-case`の結果も package-lock.json に記載されます。
（"^1.0.0" という指定に対し "v.1.1.4" がinstallされた）
```json
"node_modules/binary-case": {
  "version": "1.1.4",
  "resolved": "https://registry.npmjs.org/binary-case/-/binary-case-1.1.4.tgz",
  "integrity": "sha512-9Kq8m6NZTAgy05Ryuh7U3Qc4/ujLQU1AZ5vMw4cr3igTdi5itZC6kCNrRr2X8NzPiDn2oUIFTfa71DKMnue/Zg=="
}
```

このように、npmが依存性を自動で解決していき最終的に node_modules にパッケージを配置した結果が記載されたファイルが  package-lock.json です。

# パッケージのバージョン管理はどうすべき？

上で書いたような知識すら曖昧な状態でした。（猛省）
なので環境間でバージョン差分が発生する、という問題にも遭遇してしまったのですが、
同じ過ちを起こさないために適切なパッケージ管理について少し考えたいと思います。

まず、node_modules は重すぎるのでgit管理しないです。
その設計書の役割を果たす `package.json`と `package-lock.json`をgit管理して共有することになると思います。

この時、`npm install`での運用だと、例えば以下のように開発者間で使用バージョンの差分が発生します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3537959/e4f5c33c-db57-69ee-d4e9-f48f89a663b0.png" width=80%>

...と思っていましたが、package-lock.json が管理できていれば`npm install`でも問題なさそう...

>どうも、以前はnpm installはpackage-lock.jsonを無視していたらしい。だから、npm installを実行すると、実行したタイミングで最新のバージョンのパッケージがインストールされていたみたい。それが問題になってnpm ciが作成されたようである。ところが、この動きがnpm v7で、バグとしてfixされた様子。

引用元：https://bufferings.hatenablog.com/entry/2023/03/15/215044

ちょっと混乱してきましたが、package.json と package-lock.json の内容に齟齬がなければv7以降のnpmでは npm install でも package-lock.json の内容を元にinstallされるようです。
（実際、v9のnpmでは npm install でも package-lock.json の内容が優先されること確認済）


なので、`npm install`でも`npm ci`(package-lock.json を正としてclean install するコマンド)でも固定バージョンでの運用は問題なさそうですね。（`package-lock.json`をソース管理するという考えは大前提ですが）

`npm install`と`npm ci`どちらがいいかという話は...バージョン固定という観点ならどちらでも大丈夫そう。
ただ、コマンドの仕様としては`npm ci`の方が固い気がします。
（CI/CD環境でのキャッシュ最適化などの話が絡んでくるとまた見るべき特性が変わりますが）

まとめると...

* 初期構築時のパッケージインストールはもちろん`npm install`
* `package.json`と`package-lock.json`はgit管理
* 開発者・環境間のバージョン差異は許容すべきではない
* `package-lock.json`を正とし`npm ci(install)`でバージョン固定を実現
* 固定バージョンで運用していくのでパッケージの老朽化に注意する必要がある
* 定期的な脆弱性チェックを行いつつ、1~2年間隔？ぐらいでバージョン更新を行う

こんな考え方の運用が一般的なのかなぁ、と思いました。


* バージョン更新イベントではどのように更新対象を抽出して更新バージョンを決定するのか
* 脆弱性チェックはどう行うのか

ここら辺はまだ少し知識不足なので今回は省略しましたが、改めて記事にしたいと思います。

# 参考
* https://semver.org/lang/ja/
* https://bufferings.hatenablog.com/entry/2023/03/15/215044
* https://qiita.com/sotarok/items/4ebd4cfedab186355867