semver(Node.jsライブラリ)のチートシートと実例紹介

はじめに(執筆の動機)

GROWI.cloud は、弊社が主に開発し、 OSS として GitHub に公開されている GROWI を、 SaaS としてクラウド版提供するサービスです。
本サービスでは、サービス上で立ち上げる GROWI のバージョンを適切にコントロールするために npm で公開されている semver ライブラリを利用しています。
実装の際に調べてみたところ、https://devhints.io/semver など semver の概念のチートシートはありましたが、Node.js の semver ライブラリの日本語版で分かりやすいチートシートや参考記事が少ないなと感じたため、今後の誰かのためになるならと思い執筆しました。
ご参考になれば幸いです。

semverとは

メジャー.マイナー.パッチ(-プレリリースprefix.ブレリリース番号) の3つ(プレリリースを入れると4つ)のリリースタイプ (release type) で構成される semver.org が提唱するバージョン採番の仕様
※この記事では、あくまで Node.js の semver ライブラリチートシートとして活用されることを目的としているため、 semver の仕様自体については深く触れません。

semver(Node.jsライブラリ)のチートシート

semver が node module として用意しているメソッドのうち、よく使われる機能について紹介していきます。

以降は、 import semver from 'semver'; をしたうえで記述しているコードととらえてください。

パース&バリデーション系

文字列を semver 形式でパースする

正常にパースできたら SemVer オブジェクトを、できない場合は null を返す

semver.parse('1.2.3-alpha.1'); // SemVer Object 1.2.3-alpha.1
/**
{
    "options": {
        "loose": false,
        "includePrerelease": false
    },
    "loose": false,
    "raw": "1.2.3-alpha.1",
    "major": 1,
    "minor": 2,
    "patch": 3,
    "prerelease": [
        "alpha",
        1
    ],
    "build": [],
    "version": "1.2.3-alpha.1"
}
*/

バージョン(文字列)のバリデーション (semver のチェック)

validation OK であればパースされたバージョン文字列を、NG の場合は null を返す

semver.valid('1.2.3'); // '1.2.3'
semver.valid('a.b.c'); // null

不要な文字列を除去してバージョンの文字列のみ抽出する

semver.clean('  =v1.2.3   '); // '1.2.3'

文字列を semver に対応するバージョンに矯正する

semver.coerce('v2'); // SemVer Object 2.0.0 (SemVer オブジェクトの構成は上に出しているため割愛)
semver.coerce('42.6.7.9.3-alpha'); // SemVer Object 42.6.7 (SemVer オブジェクトの構成は先に出しているため割愛)

第一引数で渡したバージョンが、第二引数に指定した条件の範囲 (version range) を満たすかチェック

第二引数に指定できる条件 (range) の書き方は #レンジの書き方 を参照

const range = '1.x || >=2.5.0 <2.6.0 || 5.0.0 - 7.2.3';
semver.satisfies('1.2.3', range); // true
semver.satisfies('2.3.4', range); // false
semver.satisfies('2.5.4', range); // true
semver.satisfies('8.0.0', range); // false

比較系

チェック対象のバージョンが比較対象のバージョン より高い かチェック

prerelease version は、 non prerelease の version より低い

semver.gt('4.5.12', '4.5.15'); // false
semver.gt('4.5.12', '4.5.12-alpha.1'); // true
semver.gt('4.5.12', '4.5.12'); // false

チェック対象のバージョンが比較対象のバージョン より低い かチェック

semver.lt('4.5.12', '4.5.15'); // true
semver.lt('4.5.12', '4.5.12-alpha.1'); // false
semver.lt('4.5.12', '4.5.12'); // false

チェック対象のバージョンが比較対象のバージョン 以上 かチェック

semver.gte('4.5.12', '4.5.15'); // false
semver.gte('4.5.12', '4.5.12-alpha.1'); // true
semver.gte('4.5.12', '4.5.12'); // true

チェック対象のバージョンが比較対象のバージョン 以下 かチェック

semver.lte('4.5.12', '4.5.15'); // true
semver.lte('4.5.12', '4.5.12-alpha.1'); // false
semver.lte('4.5.12', '4.5.12'); // true

チェック対象のバージョンが比較対象のバージョンと同一かチェック

semver.eq('4.5.12', '4.5.15'); // false
semver.eq('4.5.12', '4.5.12-alpha.1'); // false
semver.eq('4.5.12', '4.5.12'); // true

チェック対象のバージョンと比較対象のバージョンの大小をチェック

第一引数を第二引数と比較して、小さい場合は -1, 大きい場合は 1, 合致する場合は 0 を返す

semver.compare('4.5.12', '4.5.15'); // -1
semver.compare('4.5.12', '4.5.12-alpha.1'); // 1
semver.compare('4.5.12', '4.5.12'); // 0

第二引数に指定した比較条件で、チェック対象のバージョンと比較対象のバージョンを比較した結果を得る

semver.cmp('4.5.12', '<=', '4.5.12'); // true
semver.cmp('4.5.12', '===', '4.5.12'); // true
semver.cmp('4.5.12', '!==', '4.5.12'); // false

チェック対象のバージョンと比較対象のバージョンとの差分が度のリリースタイプにあるのかをチェック

semver.diff('4.5.12', '4.5.12'); // null
semver.diff('4.5.12', '4.5.13'); // 'patch'
semver.diff('4.5.12', '4.3.13'); // 'minor'
semver.diff('4.5.12', '5.3.13'); // 'major'
semver.diff('4.5.12', '4.5.12-RC.1234543'); // 'prerelease'

バージョン操作系

バージョンをインクリメントさせる

semver.inc('4.5.12', 'prerelease', 'RC'); // '4.5.13-RC.0'
semver.inc('4.5.12', 'patch'); // '4.5.13'
semver.inc('4.5.12', 'minor'); // '4.6.0'
semver.inc('4.5.12', 'major'); // '5.0.0'

その他

各リリースタイプのバージョン番号のみ抽出

semver.major('4.5.12'); // 4
semver.minor('4.5.12'); // 5
semver.patch('4.5.12'); // 12
semver.prerelease('4.5.12'); // null
semver.prerelease('4.5.12-RC.1234543'); // ['RC', 1234543]

オプションについて

semver に用意されているすべてのメソッドは、最後の引数にオプションオブジェクトを取ります。
すべてのオプションは、デフォルトではfalseです。
オプションオブジェクトは { key: value, key2: value2 } の形で同時に複数指定できます。

オプション一覧
includePrerelease: true を指定することで、プレリリースのタグ付きバージョン指定をレンジに含めることができます。
loose: true を指定することで、たとえば v2 などのような semver として有効でない文字列を許容することができます。

import semver from 'semver';

semver.satisfies('5.0.0-RC.1', '>=5.0.0-RC.0', { includePrerelease: true }); // true
semver.satisfies('5.0.0-RC.1', '>=5.0.0', { includePrerelease: true }); // false

レンジの書き方

レンジ (version range) とは、バージョンの範囲を示す条件
ドキュメントには以下の記載があります。

  • バージョン範囲は、範囲を満たすバージョンを指定するコンパレータのセットです
    • (A version range is a set of comparators which specify versions that satisfy the range.)
  • コンパレータは、オペレータ(演算子)とバージョンで構成されます
    • (A comparator is composed of an operator and a version.)

ココでは、 semver.satisfies(v, range) (vrange を満たすかチェックするメソッド) を用いて記載していきます。
※チルダ (~1.2.3) と、キャレット (^1.2.3) の範囲指定についての紹介は、今回は省略します。

比較演算子

import semver from 'semver';

// 4.5.12 未満
semver.satifsies('4.5.12', '<4.5.12'); // false
// 4.5.12 以下
semver.satifsies('4.5.12', '=<4.5.12'); // true
// 4.5.12 より高い
semver.satifsies('4.5.12', '>4.5.12'); // false
// 4.5.12 以上
semver.satifsies('4.5.12', '=>4.5.12'); // true
// 4.5.12
semver.satifsies('4.5.12', '=4.5.12'); // true

条件の結合

' '(半角空白) または '||' を用いて、条件を結合できます。

条件のAND結合

' '(半角空白) で結合すると、結合して指定された条件全てに合致するかチェックされます。
つまり、条件を AND 結合したのと同等になります。

import semver from 'semver';

// 例)
range = '>=4.5.1 <4.5.18'; // 4.5.1 以上 かつ 4.5.18 未満

semver.satifsies('4.5.12', range); // true
semver.satifsies('4.6.2', range); // false

条件のOR結合

'||' で結合すると、結合して指定された条件のいずれかに合致するかチェックされます。
つまり、条件を OR 結合したのと同等になります。

import semver from 'semver';

// 例)
range = '4.5.18 || 4.6.2'; // 4.5.18 または 4.6.2

semver.satifsies('4.5.12', range); // false
semver.satifsies('4.5.18', range); // true
semver.satifsies('4.6.2', range); // true

結合の優先順

一般的な ANDOR と優先順は変わりません。

import semver from 'semver';

// 例)
range = '4.6.2 || >=4.5.1 <4.5.18'; // 4.6.2 または (4.5.1 以上 かつ 4.5.18 未満)

semver.satifsies('4.5.12', range); // true
semver.satifsies('4.5.19', range); // false
semver.satifsies('4.6.2', range); // true

-(ハイフン)範囲指定

'-'(ハイフン) つなぎでバージョンの範囲を指定することができます。
記法: 低いバージョン - 高いバージョン

import semver from 'semver';

// 例)
range = '4.5.12 - 5.0.4'; // 4.5.12 以上 ~ 5.0.4 未満

semver.satifsies('4.5.12', range); // true
semver.satifsies('4.5.11', range); // false
semver.satifsies('4.6.2', range); // true
semver.satifsies('5.0.4', range); // false

ワイルドカード指定

x, X, * のいずれかを使用して、[メジャー, マイナー, パッチ] の数値の1つを「代用」することができます。
また、番号の記載が無い場合は、 メジャー > マイナー > パッチ の順で数値があてはめられ、不足分は * で補完されます。

1, 1.x, 1.*.* : すべて >=1.0.0 <2.0.0 と同義

GROWI.cloudでの実用例

実用例1:選択可能なバージョンの制限

GROWI では、 v4 系から v5 系バージョンへのアップグレードで、バージョン更新が不可逆となる変更がが入りました。
その際に、現在設定されているバージョンと変更先のバージョンをチェックしてバージョンの変更可否を判断する機構が実装されています。

import semver from 'semver';

/**
 * バージョン変更の可否をチェックし、バージョン変更ができない場合はその旨を説明するメッセージを配列で返す
 *
 * @param {string} fromGrowiVersion 変更前の現在のバージョン
 * @param {string} toGrowiVersion 選択中の変更後のバージョン
 * @param {Array<GrowiVersionChangeRestrictionConfig>} configs
 * @returns {Array<string>} バージョン変更の拒否メッセージ(リスト)
 */
const getVersionChangeRestrictedMessages = (fromGrowiVersion, toGrowiVersion, configs) => {
  return configs.filter((config) => {
    // RC 版含め変更前の GROWI のバージョンが sourceVersionRange に合致するか
    return semver.satisfies(fromGrowiVersion, config.sourceVersionRange, { includePrerelease: true })
    // RC 版含め変更先の GROWI のバージョンが targetVersionRange に合致するか
        && semver.satisfies(toGrowiVersion, config.targetVersionRange, { includePrerelease: true });
  }).map((config) => { return config.message });
};

実用例2:バージョンの自動アップグレード機能

バージョンの自動アップグレード機能のバッチ処理で、最新のバージョンがリリースされたときに releasetAt 基準の最新バージョンではなく、 SemVer の概念における「最新のバージョン」が特定できるよう実装しています。

GrowiAppVersion テーブル のレコード例 (リリース日の降順)

version releasedAt isStable isVisible
4.5.18 2022-04-15 08:25:16 0 1
5.0.1 2022-04-15 08:44:08 0 1
4.5.17 2022-04-07 10:06:15 0 1
4.5.16 2022-04-06 09:46:40 1 1
5.0.0 2022-04-01 16:08:24 0 1
5.0.0-RC.14 2022-03-30 13:24:37 0 0

実装

import semver from 'semver';
import { GrowiAppVersion } = from 'models';

/**
 * アップデート可能なバージョンのリストを返す
 * Semantic Versioning によって、バージョンの降順にソート
 *
 * @returns {Promise<Array<GrowiAppVersion>} semver の降順にソート
 */
async getAvailableGrowiAppVersions() {
    const growiAppVersions = await GrowiAppVersion.findAll({ where: { isVisible: true } });

    return growiAppVersions.sort((v1, v2) => {
        return semver.rcompare(v1.version, v2.version, { includePrerelease: true });
    });
}

/**
 * 「常に最新のバージョンを利用する」 GrowiApp 全てのバージョンを更新する
 */
async updateGrowiAppsVersionBatch() {
    // 利用可能なGrowiAppVersionsを取得
    const availableGrowiAppVersions = await getAvailableGrowiAppVersions();
    // 安定版 の最新 version を取得
    const latestStableVersionObj = availableGrowiAppVersions.find((val) => { return val.isStable });
    // テスト版も含む 最新 version を取得
    const latestVersionObj = availableGrowiAppVersions.find((val) => {
        return !isRcWithHashVersion(val.version); // -RC は含まない
    });

    // 安定版 の最新 version
    console.log(latestStableVersionObj.version); // 4.5.16
    // テスト版も含む 最新 version
    console.log(latestVersionObj.version); // 5.0.12

    ... // バージョン更新処理

}

semverで引っかかったポイント

semver を使った実装を終えた今となっては理解できることですが、「5.0.0-RC.1 というプレリリースのバージョンが 5.0.0 未満 のバージョンと判定される」という pre-release バージョンの扱いに当初戸惑いました。

なぜなら、 GROWI v5 から v4 へのバージョンダウングレードを拒否したい場合に、変更前のバージョンが (プレリリースを含む) v5 系かつ、変更後のバージョンが v4 系であることを判定するために以下の判定を行っても v5 RC バージョンを v5 系バージョンと判別することができず、v4系バージョンとして扱われているような挙動に見えたためです。

  • 変更前バージョン判定:
    semver.satisfies('5.0.0-RC.0',  '>=5'); // false ← `true` と判定したい
  • 変更先バージョン判定:
    semver.satisfies('5.0.0-RC.0',  '<5'); // true ← `false` と判定したい

最終的には、下記の判定とすることで、想定した挙動を得ることができました。

  • 変更前バージョン判定:
    semver.satisfies('5.0.0-RC.0', '>=5.0.0-RC.0', { includePrerelease: true }); // true  ← `true` を想定
  • 変更先バージョン判定:
    semver.satisfies('5.0.0-RC.0', '<5.0.0-RC.0', { includePrerelease: true }); // false  ← `false` を想定

最後に

semver の仕様を理解しきれておらずに悩まされた部分もありましたが実装を経て理解できた部分があったので、似たような問題を抱えている方がいた際の助けになれば幸いです。
最後までご拝読いただきありがとうございました。

参考

今回の記事は、こちらのリンク先を参考に執筆しました。