Angka versi seperti 2.5.1 tampak seperti bilangan desimal, tetapi perilakunya sama sekali berbeda — 2.10.0 justru lebih baru daripada 2.9.1. Ekosistem Node.js bergantung pada kesepakatan membaca angka versi bernama Semantic Versioning (SemVer): pembuat package berjanji menaikkan angka yang tepat setiap kali kode berubah, dan pemakai package memakai janji tersebut untuk memperbarui dependencies dengan aman. Notasi ^5.1.0 yang kita lihat pada field dependencies adalah bahasa ringkas dari janji itu. Chapter ini membahas cara membaca, membandingkan, dan menuliskan versi — lengkap dengan rentang sirkonfleks dan tilde.

Tiga Angka dan Artinya #

Versi SemVer selalu berformat MAJOR.MINOR.PATCH — tiga angka non-negatif yang dipisah titik. Setiap angka naik dengan aturan yang berbeda, dan aturan itulah sumber nama semantic: perubahan angka membawa makna tentang seberapa besar kode berubah.

Angka naik ketikaSifat perubahanContoh
MAJORPerubahan yang merusak kode pemakai lamaNama function berubah, argument dihapus
MINORFitur baru yang kompatibel dengan kode lamaFunction baru ditambahkan
PATCHPerbaikan cacat yang kompatibel dengan kode lamaBug diperbaiki tanpa mengubah API

Sebuah library pada versi 2.5.1 yang menambah function baru akan rilis sebagai 2.6.0; memperbaiki bug menjadi 2.5.2; dan mengganti nama function — yang memaksa semua pemakai mengubah kode mereka — menjadi 3.0.0. Kita bedah format ini dengan parser sederhana:

js
function bacaVersi(versi) {
  const [angka, prarilis] = versi.split("-");
  const [major, minor, patch] = angka.split(".").map(Number);
  return { versi, major, minor, patch, prarilis };
}

for (const v of ["2.5.1", "1.0.0-beta.1"]) {
  const hasil = bacaVersi(v);
  console.log(`${hasil.versi} -> MAJOR=${hasil.major} MINOR=${hasil.minor} PATCH=${hasil.patch}`);
  console.log(`  prarilis: ${hasil.prarilis ?? "(tidak ada)"}`);
}

Output yang Dihasilkan:

text
2.5.1 -> MAJOR=2 MINOR=5 PATCH=1
  prarilis: (tidak ada)
1.0.0-beta.1 -> MAJOR=1 MINOR=0 PATCH=0
  prarilis: beta.1

Mari kita telaah baris-baris kode di atas:

  1. Baris 2 (versi.split("-")): tanda hubung memisahkan tiga angka utama dari label prarilis seperti beta.1. Versi stabil tidak memuat tanda hubung, sehingga prarilis bernilai undefined.
  2. Baris 3 (.split(".").map(Number)): tiga angka dipecah lalu masing-masing dikonversi dari string menjadi number — langkah ini penting, alasannya terlihat pada section berikutnya.
  3. Baris 9 (hasil.prarilis ?? "(tidak ada)"): operator nullish coalescing menggantikan undefined dengan teks yang ramah dibaca. Prarilis memiliki aturan tambahan: versi berlabel prarilis selalu dianggap lebih lama daripada versi rilis dengan tiga angka yang sama.

Membandingkan Versi dengan Benar #

Karena tiap bagian versi adalah angka tersendiri, perbandingan harus dilakukan angka demi angka — bukan sebagai satu string. Operator < pada JavaScript membandingkan string huruf demi huruf, dan di sanalah jebakannya:

js
function bandingkan(a, b) {
  const bagianA = a.split(".").map(Number);
  const bagianB = b.split(".").map(Number);
  for (let i = 0; i < 3; i++) {
    if (bagianA[i] !== bagianB[i]) {
      return bagianA[i] > bagianB[i] ? 1 : -1;
    }
  }
  return 0;
}

const x = "2.10.0";
const y = "2.9.1";

console.log(`perbandingan string : "${x}" < "${y}" ->`, x < y);
console.log(`perbandingan angka  : bandingkan("${x}", "${y}") ->`, bandingkan(x, y));
console.log(`"${x}" lebih ${bandingkan(x, y) === 1 ? "baru" : "lama"} dari "${y}"`);

Output yang Dihasilkan:

text
perbandingan string : "2.10.0" < "2.9.1" -> true
perbandingan angka  : bandingkan("2.10.0", "2.9.1") -> 1
"2.10.0" lebih baru dari "2.9.1"

Perbandingan string menghasilkan true karena karakter "1" pada "2.10.0" memang lebih awal daripada "9" — padahal 10 lebih besar daripada 9. Function bandingkan() menghindari masalah ini dengan membandingkan major dulu, lalu minor, lalu patch, dan hanya melanjutkan ke angka berikutnya saat angka saat ini sama. Nilai balikan mengikuti kebiasaan strcmp: 1 bila versi pertama lebih baru, -1 bila lebih lama, dan 0 bila keduanya identik.

Rentang Versi pada dependencies #

Field dependencies jarang menuliskan satu versi persis. Package pemakai biasanya menuliskan rentang versi agar perbaikan PATCH dan fitur MINOR mengalir otomatis lewat npm install, sementara perubahan MAJOR yang berpotensi merusak kode tetap tertahan. Sintaks rentang yang paling sering jumpa:

NotasiRentang yang diizinkan
1.4.2Hanya versi 1.4.2 persis
^1.4.2>=1.4.2 <2.0.0
~1.4.2>=1.4.2 <1.5.0
>=1.4.2 <2.0.0Rentang eksplisit dengan batas atas terbuka
1.2.3 - 2.3.4>=1.2.3 <=2.3.4, batas atas ikut termasuk
*Versi apa pun

Dua notasi terpenting mudah diingat sebagai janji: sirkonfleks ^ berarti “boleh naik selama MAJOR tidak berubah”, dan tilde ~ berarti “boleh naik selama MINOR tidak berubah”. Keduanya kita terjemahkan menjadi rentang eksplisit dengan program berikut:

js
function rentangCaret(versi) {
  const [major, minor, patch] = versi.split(".").map(Number);
  if (major > 0) {
    return `>=${versi} <${major + 1}.0.0`;
  }
  if (minor > 0) {
    return `>=${versi} <0.${minor + 1}.0`;
  }
  return `>=${versi} <0.0.${patch + 1}`;
}

function rentangTilde(versi) {
  const [major, minor] = versi.split(".").map(Number);
  return `>=${versi} <${major}.${minor + 1}.0`;
}

const contoh = ["1.4.2", "0.2.5", "0.0.3", "2.5.1"];

for (const versi of contoh) {
  console.log(`^${versi.padEnd(7)} -> ${rentangCaret(versi)}`);
  console.log(`~${versi.padEnd(7)} -> ${rentangTilde(versi)}`);
}

Output yang Dihasilkan:

text
^1.4.2   -> >=1.4.2 <2.0.0
~1.4.2   -> >=1.4.2 <1.5.0
^0.2.5   -> >=0.2.5 <0.3.0
~0.2.5   -> >=0.2.5 <0.3.0
^0.0.3   -> >=0.0.3 <0.0.4
~0.0.3   -> >=0.0.3 <0.1.0
^2.5.1   -> >=2.5.1 <3.0.0
~2.5.1   -> >=2.5.1 <2.6.0

Perhatikan baris ^0.2.5 — hasilnya >=0.2.5 <0.3.0, sama persis dengan tilde. Function rentangCaret menangani kasus khusus ini secara sengaja karena npm menerapkan aturan tambahan untuk versi di bawah 1.0.0:

Rentang pada package.json dan versi terkunci pada package-lock.json bekerja berlapis: npm install memilih versi tertinggi yang masuk rentang lalu membekukannya di lock, dan instalasi berikutnya mengikuti lock tersebut. Naikkan batas rentang hanya bila kita memang siap menerima perubahan — misalnya menulis ulang ^1.4.2 menjadi ^2.0.0 setelah membaca catatan rilis major baru.

Aturan versi kini lengkap kita kuasai, dan satu manfaat langsungnya menanti pada chapter berikutnya: perintah-perintah npm yang kita jalankan setiap hari sebenarnya adalah pintasan menuju script yang tercatat di package.json. Kita bedah mekanisme npm run pada Chapter npm Scripts.