מה זה Semantic Versioning ואיך הוא עובד ב-npm

סער טויטו

סער טויטו

Software & Web Development


TL;DR
  • Semantic Versioning מגדיר פורמט MAJOR.MINOR.PATCH — כל מספר מספר סיפור על מה השתנה בגרסה החדשה.
  • PATCH / MINOR / MAJOR תיקון באג = PATCH עולה; פיצ׳ר חדש = MINOR עולה; שינוי שובר = MAJOR עולה.
  • npm ו-^ ו-~ הסימן ^ מאפשר עדכוני MINOR+PATCH אוטומטיים; ~ מאפשר PATCH בלבד — לפי semver.
  • peerDependencies ספרייה שמצהירה peerDependencies אומרת: "השתמש בחבילה שלך, לא בשלי" — למניעת כפילויות.

Semantic Versioning במבט כללי

Semantic Versioning הוא שפה משותפת לניהול גרסאות תוכנה. כל מספר בפורמט MAJOR.MINOR.PATCH נושא משמעות ברורה: PATCH לתיקוני באגים, MINOR לפיצ׳רים חדשים שאינם שוברים קוד קיים, ו-MAJOR לשינויים שדורשים עדכון קוד. npm מאמץ סטנדרט זה לניהול תלויות ב-package.json.

הקדמה

בכל פעם שאנחנו מתקינים חבילה ב-npm, משדרגים ספרייה, או מפרסמים גרסה חדשה של הפרויקט שלנו - יש שם מספר שנראה בערך כך: 2.4.1. רוב המפתחים מכירים את המבנה הזה, אבל לא תמיד יודעים מה כל מספר אומר באמת.

האם לעלות מ-1.0.0 ל-1.0.1 זה אותו דבר כמו לעלות ל-1.1.0? ממש לא. יש פה שפה שלמה שנקראת Semantic Versioning - ובמאמר הזה נסביר אותה מהיסוד.

מה זה Semantic Versioning?

Semantic Versioning, או בקצרה semver, הוא סטנדרט פתוח לניהול גרסאות תוכנה שפורסם על ידי Tom Preston-Werner (אחד ממייסדי GitHub). הוא מגדיר בדיוק איך לבחור מספר גרסה - כך שכל מפתח בעולם יוכל להבין מייד מה השתנה בגרסה חדשה, מבלי לקרוא את ה-changelog.

הסטנדרט המלא זמין באתר הרשמי semver.org, ו-npm מאמצת אותו כחלק מרכזי בניהול חבילות - כפי שניתן לראות בתיעוד הרשמי של npm.

הרעיון הבסיסי: מספר גרסה צריך לספר סיפור. הוא לא רק "עוד מספר" - הוא הבטחה מהמפתח לכל מי שמשתמש בספרייה.

שלושת המספרים: MAJOR.MINOR.PATCH

כל גרסה בנויה משלושה מספרים בפורמט X.Y.Z, כאשר כל אחד מהם מייצג רמה שונה של שינוי:

PATCH - תיקון באג (המספר הימני)

עולה כשמתקנים באג בצורה שלא שוברת שום דבר קיים. אם משתמשים בגרסה 2.4.1 וצריך לתקן באג קטן - הגרסה החדשה תהיה 2.4.2. הקוד של כל מי שמשתמש בספרייה ממשיך לעבוד בדיוק אותו דבר.

לפי המפרט של semver.org:

"PATCH version when you make backward compatible bug fixes."

MINOR - תוספת חדשה (המספר האמצעי)

עולה כשמוסיפים פונקציונליות חדשה שלא שוברת קוד קיים. אם הוספנו פיצ׳ר חדש לספרייה - המעבר יהיה מ-2.4.1 ל-2.5.0. שימו לב: ה-PATCH מתאפס חזרה לאפס.

לפי המפרט:

"MINOR version when you add functionality in a backward compatible manner."

המשמעות הפרקטית: כל מי שמשתמש בגרסה הישנה ממשיך לעבוד בלי שום שינוי. הם פשוט יכולים לבחור להשתמש בפיצ׳ר החדש - אם הם רוצים.

MAJOR - שינוי שובר (המספר השמאלי)

עולה כשמבצעים שינוי שעלול לשבור קוד קיים. זה מה שנקרא Breaking Change. אם שינינו את ה-API של פונקציה קיימת, הסרנו פרמטר, או שינינו את ה-return value - זה MAJOR.

המעבר יהיה מ-2.4.1 ל-3.0.0. גם ה-MINOR וגם ה-PATCH מתאפסים.

לפי המפרט:

"MAJOR version when you make incompatible API changes."

הכלל הפשוט לזכור

יש דרך קלה לזכור את שלושת הרמות:

  • 1.0.0 1.0.1 - "תיקנו באג, כלום לא נשבר"
  • 1.0.0 1.1.0 - "הוספנו משהו חדש, כלום לא נשבר"
  • 1.0.0 2.0.0 - "שינינו משהו שעלול לשבור קוד קיים"

כאשר MINOR עולה - ה-PATCH מתאפס. כאשר MAJOR עולה - גם MINOR וגם PATCH מתאפסים. כך תמיד נדע שגרסה 2.0.0 היא "נקודת פתיחה חדשה".

גרסה 0.y.z - שלב הפיתוח הראשוני

יש כלל נוסף שהרבה מפתחים לא מכירים: כשמספר ה-MAJOR הוא 0 (למשל 0.4.2), הספרייה נחשבת עדיין בשלב פיתוח ראשוני.

לפי semver.org:

"Major version zero (0.y.z) is for initial development. Anything MAY change at any time. The public API SHOULD NOT be considered stable."

במילים פשוטות: כשרואים גרסה שמתחילה ב-0, זה אומר שהכל עדיין יכול להשתנות ואין הבטחה לאחריות לאחור. הגרסה 1.0.0 היא זו שמכריזה רשמית: "הספרייה יציבה, ה-API מוגדר, אפשר לסמוך עלינו."

איך npm משתמש ב-Semantic Versioning

אם אתם עובדים עם Node.js, כנראה ראיתם קבצי package.json עם תלויות שנראות כך:

"dependencies": {
  "react": "^18.2.0",
  "lodash": "~4.17.21"
}

הסימנים ^ ו-~ הם מה שנקרא version ranges - טווחי גרסאות. הם אומרים ל-npm אילו עדכונים מותר להתקין אוטומטית.

הסימן ^ (Caret) - עדכוני MINOR ו-PATCH בלבד

הסימן ^ לפני גרסה אומר: "תתקין את הגרסה הזו, ואפשר לעדכן אוטומטית כל עוד ה-MAJOR לא משתנה."

לדוגמה, ^18.2.0 יתקין כל גרסה בין 18.2.0 ל-19.0.0 (לא כולל). כלומר, עדכוני MINOR ו-PATCH יותקנו אוטומטית, אבל גרסה 19.x.x לא - כי היא עלולה לשבור קוד.

הסימן ~ (Tilde) - עדכוני PATCH בלבד

הסימן ~ שמרן יותר. הוא אומר: "תתקין רק עדכוני PATCH."

לדוגמה, ~4.17.21 יתקין כל גרסה בין 4.17.21 ל-4.18.0 (לא כולל). רק תיקוני באגים - ללא תוספות חדשות.

לפי תיעוד npm, הם ממליצים להתחיל חבילות חדשות מגרסה 1.0.0 ולדבוק בסטנדרט של semver על מנת שמשתמשי הספרייה יוכלו לנהל תלויות בצורה בטוחה.

peerDependencies - כשספרייה שואלת מכם חבילה

מעבר ל-dependencies ו-devDependencies, יש עוד סוג אחד שחשוב להכיר: peerDependencies. הוא פחות מוכר, אבל מאוד נפוץ בספריות.

האנלוגיה: מקדחה וסוללה

תדמיינו שיש ספרייה שנקראת UIComponents. היא כלי עבודה - כמו מקדחה. כדי לעבוד, המקדחה צריכה סוללה.

אבל המקדחה לא מגיעה עם סוללה משלה - היא מצפה שכבר יהיה לכם אחת. היא פשוט אומרת: "אני צריכה סוללה מסדרת 18V - כל סוללה 18V מתאימה (^18V)."

זה בדיוק peerDependencies. הספרייה (UIComponents) משתמשת ב-axios שלכם - לא בשלה.

למה ספרייה לא יכולה פשוט לכלול axios משלה?

טכנית - היא יכולה. אבל זה יגרום לבאג. הנה למה:

נניח שהפרויקט שלכם (ICT) מתקין axios 1.15.2, ו-UIComponents מכלילה בתוכה axios 1.15.0 נפרד. עכשיו יש לכם שני עותקים של axios שרצים באותה אפליקציה. הם לא מכירים אחד את השני.

זה גורם לבעיות כמו:

  • Interceptors שהגדרתם ב-axios שלכם לא יחולו על בקשות שנשלחות מתוך UIComponents
  • הגדרות axios משותפות (headers, baseURL) מתעלמות
  • האפליקציה הופכת לבלתי צפויה

לכן ההחלטה היא: "לא נכלול axios - נשתמש באחד שכבר קיים אצל מי שמתקין אותנו." וזה בדיוק מה ש-peerDependencies אומר.

מתי peerDependencies נוצר ב-package.json?

זה ההבדל החשוב: peerDependencies אף פעם לא נוצר אוטומטית על ידי npm.

כשמריצים npm install axios בפרויקט רגיל - הוא הולך ל-dependencies אוטומטית. אבל peerDependencies הוא הצהרה שמחברי הספרייה כותבים ידנית ב-package.json כדי לומר: "מי שמתקין אותנו - וודאו שיש לכם את החבילה הזו מותקנת בעצמכם."

"peerDependencies": {
  "axios": "^1.15.0",
  "react": "^18.0.0"
}

מה קורה ב-ICT כשמתקינים את UIComponents?

כשמריצים npm install shared-uicomponents-library-v2 ב-ICT, npm מוריד את UIComponents ורואה את ה-peerDependencies שלה. הוא בודק: האם ל-ICT כבר יש axios ו-react מותקנים?

ה-package.json של ICT נשאר בדיוק כמו שהוא - UIComponents נכנסת ל-dependencies כמו כל חבילה אחרת. שום peerDependencies לא נוצר ב-ICT:

"dependencies": {
  "shared-uicomponents-library-v2": "^2.3.1",
  "axios": "1.15.2",   // ה-axios של ICT — UIComponents תשאל ממנו
  "react": "^18.0.0"
}

npm רק בודק שמה ש-UIComponents צריכה כבר קיים. אם כן - מצוין, הם ישתפו אותו. אם לא - npm מגיב בהתאם לגרסה שלו.

npm v7+ לעומת גרסאות ישנות יותר

ההתנהגות כשיש בעיה עם peer dependency השתנתה בין גרסאות npm, לפי התיעוד הרשמי:

  • npm v3–v6 — peer dependencies לא מותקנות אוטומטית. אם חסרות, npm רק מדפיס אזהרה.
  • npm v7+ — peer dependencies מותקנות אוטומטית אם הן חסרות לגמרי. אבל אם כבר קיימת גרסה שמתנגשת עם מה שהספרייה דורשת — npm זורק שגיאת ERESOLVE ומסרב להתקין.

זה בדיוק השגיאה שחבר הצוות קיבל: ל-ICT היה axios 1.15.2 מותקן, אבל UIComponents הצהירה על "axios": "1.15.0" בלי ^. npm v7 ראה קונפליקט גרסאות וסירב להמשיך.

לכן הכלל: תמיד להשתמש ב-^ ב-peerDependencies, כדי שהבדלים קטנים כמו 1.15.0 מול 1.15.2 לא יגרמו לשגיאה.

לסיכום: ספרייה שמשתמשת ב-peerDependencies אומרת לכם "יש לי import axios בקוד שלי, אבל לא שיליבתי אותו - תוודאו שיש לכם axios מותקן, ואני אשתמש בשלכם."

גרסה שפורסמה - אסור לשנות אותה

יש כלל ברזל אחד בסמנטיק ורז'נינג שחשוב לדעת: גרסה שפורסמה היא קבועה לנצח.

לפי semver.org:

"Once a versioned package has been released, the contents of that version MUST NOT be modified."

אם גיליתם באג אחרי שפרסמתם 2.4.1 - אסור לשנות את 2.4.1. צריך לפרסם גרסה חדשה: 2.4.2. הסיבה פשוטה: אנשים שהתקינו 2.4.1 מצפים שהיא תישאר זהה. אם תשנו גרסה שכבר פורסמה, אתם שוברים את האמון של כל מי שמשתמש בספרייה שלכם.

סיכום

Semantic Versioning הוא לא רק קונבנציה - הוא שפה משותפת שמאפשרת לאקוסיסטם שלם של ספריות וכלים לעבוד ביחד בצורה צפויה. כשכולם עומדים בסטנדרט, npm יכול להחליט אוטומטית אילו עדכונים בטוחים להתקין, ואתם יכולים לסמוך על כך שעדכון PATCH לא יפרק לכם את הפרויקט.

שלושת הכללים לזכור:

  • תיקון באג PATCH עולה
  • פיצ׳ר חדש שלא שובר כלום MINOR עולה, PATCH מתאפס
  • שינוי שובר קוד קיים MAJOR עולה, MINOR ו-PATCH מתאפסים

שאלות נפוצות על Semantic Versioning

מה זה Semantic Versioning ולמה זה חשוב?

Semantic Versioning (semver) הוא סטנדרט שמגדיר שלושה מספרים: MAJOR.MINOR.PATCH. כל מספר מספר מה השתנה: PATCH = תיקון באג, MINOR = פיצ׳ר חדש שאינו שובר, MAJOR = שינוי שעלול לשבור קוד קיים. npm מאמץ את הסטנדרט כדי לנהל עדכוני תלויות בצורה בטוחה.

מה ההבדל בין ^ ל-~ ב-package.json?

הסימן ^ (caret) מאפשר עדכוני MINOR ו-PATCH אוטומטיים, כל עוד ה-MAJOR לא משתנה. הסימן ~ (tilde) שמרני יותר ומאפשר רק עדכוני PATCH. לדוגמה: ^18.2.0 יתקין עד 19.0.0 (לא כולל), ואילו ~4.17.21 יתקין עד 4.18.0 (לא כולל).

מה זה Breaking Change ב-Semantic Versioning?

Breaking Change הוא שינוי ב-API שעלול לשבור קוד שכתבתם כבר. לדוגמה: הסרת פרמטר מפונקציה, שינוי ה-return value, או שינוי שם. כל Breaking Change מחייב העלאה של מספר ה-MAJOR — מ-2.4.1 ל-3.0.0.

מה זה peerDependencies ב-npm?

peerDependencies היא הצהרה שספרייה כותבת ב-package.json שלה, שאומרת: "אני משתמשת בחבילה X שלך — לא בשלי". זה מונע שיהיו שני עותקים של אותה חבילה. npm v7+ יתקין peerDependencies אוטומטית אם חסרות, אך יזרוק שגיאת ERESOLVE אם יש קונפליקט גרסאות.

האם אפשר לשנות גרסה שכבר פורסמה ב-npm?

לא. לפי מפרט semver: "Once a versioned package has been released, the contents of that version MUST NOT be modified." אם גיליתם באג אחרי שפרסמתם 2.4.1 — חובה לפרסם גרסה חדשה 2.4.2. שינוי גרסה שפורסמה שובר את האמון של כל מי שמשתמש בה.