• בלוג
  • היום למדתי: אי אפשר להכריח לקוח להתקין ספריות Node.JS (אבל אפשר להתקרב לזה)

היום למדתי: אי אפשר להכריח לקוח להתקין ספריות Node.JS (אבל אפשר להתקרב לזה)

17/12/2021

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

הסיפור הוא פשוט - אם אתם כותבים חבילת Node.JS אז אתם יכולים לציין 3 רשימות של תלויות ב package.json שלכם:

  1. רשימת dependencies מגדירה רשימה של ספריות שאתם דורשים שהלקוח יתקין ספציפית עבורכם כשהוא מתקין את הספריה שלכם.

  2. רשימת peerDependencies מגדירה רשימה של ספריות שאתם דורשים ש"יהיו שם" כשלקוח מתקין את הספריה שלכם, כלומר ספריות שגם אתם משתמשים בהן וגם קוד הלקוח משתמש בהן.

  3. רשימת devDependencies מגדירה רשימה של תלויות שרק אתם מתקינים במצב פיתוח.

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

1. שימוש ב devDependencies

אם אני מגדיר את ריאקט בתור devDependency באמצעות הבלוק הבא בקובץ package.json בספריה שלי:

"devDependencies": {
  "react": "^17.0.2"
}

אז כשאני אעלה את הספריה ל npm ומישהו אחר יפעיל npm install עליה, אותו מישהו אחר לא יקבל את react בכלל. ריאקט היתה בשימוש רק בזמן שפיתחתי את הספריה ולא בזמן השימוש בה.

2. שימוש ב dependencies

אם אני מגדיר את ריאקט בתור dependency באמצעות הבלוק הבא בקובץ package.json בספריה שלי:

"dependencies": {
  "react": "^17.0.2"
}

אז הלקוח שמתקין את הספריה שלי יקבל בתיקיית node_modules שלו את הספריות הבאות:

node_modules
├── js-tokens
├── loose-envify
├── mylib
├── object-assign
└── react

וזה כבר נראה טוב! כלומר הלקוח קיבל גם את ריאקט וגם את כל הספריות שריאקט תלויה בהן. אבל כדאי לצנן את ההתלהבות. זה עובד רק בגלל שהלקוח תלוי רק בספריה שלי. אם הוא יוסיף תלות בספריה שצריכה גירסה ישנה יותר של ריאקט, או אפילו תלות ספציפית בגירסה ישנה יותר של ריאקט החיים הולכים להסתבך.

נניח שקוד הלקוח שלי כולל את הבלוק הבא בקובץ package.json שלו:

  "dependencies": {
    "mylib": "file:../mylib/mylib-1.0.1.tgz",
    "react": "16.8.1"
  }

אז עכשיו בהפעלה של npm install הוא יקבל בתיקיית node_modules את:

node_modules
├── js-tokens
├── loose-envify
├── mylib
├── object-assign
├── prop-types
├── react
├── react-is
└── scheduler

שזה נראה כאילו זה טוב כי הוא התקין את ריאקט אבל האמת שהוא התקין שתי גירסאות של ריאקט:

$ npm ls react
myapp@1.0.0 /Users/ynonp/tmp/mynpm/myapp
├─┬ mylib@1.0.1
│ └── react@17.0.2
└── react@16.8.1

ובאמת בתוך תיקיית node_modules/mylib אני אמצא תיקיית node_modules נוספת שבתוכה יש את ריאקט בגירסה 17.0.2, ובתיקיית node_modules הראשית אני מוצא את ריאקט בגירסה 16.8.1.

לכן הגדרת dependencies לא באמת מאפשרת לי "להכריח" לקוח להתקין ספריה בגירסה מסוימת. אם ספריה אחרת שהוא תלוי בה, או הוא עצמו, מבקשים גירסה אחרת אז זו תקבל עדיפות והגירסה שאני תלוי בה תותקן בצורה פרטית ב node_modules של הספריה שלי.

3. שימוש ב peerDependencies

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

ובאמת ההתנהגות של peerDependencies היא הפעם ממש מוצלחת. אם יש לי ב package.json של הספריה שלי את הבלוק הבא:

"peerDependencies": {
  "react": "^17.0.2"
}

והלקוח שלי מחזיק קובץ package.json עם בלוק כזה:

"dependencies": {
  "mylib": "file:../mylib/mylib-1.0.1.tgz",
  "react": "16.8.1"
}

אז כשהוא ינסה להתקין הוא יקבל את ההודעה:

$ npm install
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! While resolving: myapp@1.0.0
npm ERR! Found: react@16.8.1
npm ERR! node_modules/react
npm ERR!   react@"16.8.1" from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer react@"^17.0.2" from mylib@1.0.2
npm ERR! node_modules/mylib
npm ERR!   mylib@"file:../mylib/mylib-1.0.2.tgz" from the root project
npm ERR!
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force, or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
npm ERR!
npm ERR! See /Users/ynonp/.npm/eresolve-report.txt for a full report.

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/ynonp/.npm/_logs/2021-12-16T20_16_00_413Z-debug.log

רואים מה הבעיה כאן? נכון ש npm install נכשל, אבל הוא גם מציע שני פיתרונות שאת שניהם אנשים נוטים להפעיל כמעט בלי לחשוב. כלומר אחרי שמישהו שרוצה להשתמש בספריה שלכם רואה הודעה כזאת האינסטינקט הראשון הוא לנסות להפעיל:

$ npm install --force

שכמובן עובד עם Warnings, ומתקין את גירסה 16.8.1 של ריאקט בתיקיית node_modules הראשית ולא מתקין כלל את גירסה 17:

$ npm ls react

myapp@1.0.0 /Users/ynonp/tmp/mynpm/myapp
├─┬ mylib@1.0.2
│ └── react@16.8.1 deduped invalid: "^17.0.2" from node_modules/mylib
└── react@16.8.1 invalid: "^17.0.2" from node_modules/mylib

npm ERR! code ELSPROBLEMS
npm ERR! invalid: react@16.8.1 /Users/ynonp/tmp/mynpm/myapp/node_modules/react

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/ynonp/.npm/_logs/2021-12-16T20_18_56_638Z-debug.log

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

  1. הגדירו את הדברים שאתם רוצים שאחרים יתקינו בתור Peer Dependencies.

  2. אם קוד לקוח שמשתמש בספריה לא עובד, השתמשו ב npm ls כדי לוודא שהתלויות שרציתם באמת מותקנות כמו שצריך.

  3. תקנו את התלויות ואל תשתמשו ב --force, אפילו שזה עובד.