پکیج‌منجر NPM چطوری می‌تونه ما رو توی مشکل بندازه؟


اول بگم که فانکشن require در حالتی که ما بخوایم یک ماژول رو ایمپورت کنیم باهاش، اول میاد توی فولدر node_modules که کنار فایل index.js هست میگرده، اگه نبود توی ماژول‌های کناری می‌گرده.


وقتی توی پروژه جاوا‌اسکریپت و نود‌جی‌اس کامند npm install رو اجرا می‌کنیم، NPM شروع می‌کنه به خوندن فایل package.json‍ و تمام دیپندنسی‌هایی که اونجا تعریف شده رو از توی اینترنت دانلود و توی فولدر node_modules قرار می‌ده. وقتی داخل برنامه زیر کامند npm install رو اجرا می‌کنیم:


// package.json
{
	"name": "my app",
	"dependencies": {
		"a": "1.0.0",
		"b": "1.0.0"
	}
}



چنین ساختاری برای ما تولید می‌کنه:



  • ---- my app
  • -------- node_modules
  • ------------ a
  • ------------ b



خیلی هم عالی. اما گاهی اوقات پیش می‌آد که یکی از دیپندنسی‌ها خودش به یه پکیج دیگه وابسته‌ست و NPM مجبوره برای راضی نگه داشتن اون، دیپندنسی‌های اون رو هم دانلود کنه. توی این مثال، فرض کنید پکیج a خودش وابسته به پکیج c و d باشه و پکیج b دوباره وابسته به پکیج d باشه. ساختار فولدر ما میشه:



  • ---- my app
  • -------- node_modules
  • ------------ a
  • ---------------- node_modules
  • -------------------- c
  • -------------------- d
  • ------------------------ node_modules
  • ---------------------------- b
  • ------------ b




خوبی این ساختار اینه که اگه پکیج d داخل خودش بخواد پکیج b رو ایمپورت کنه، اون b داخلیه بهش می‌رسه و اگه ما توی روت پروژه همون کار کنیم، اون b که توی node_modules هست بهمون تحویل داده می‌شه. اما بدی‌هایی داشت این روش که باعث شد NPM روش تولید این ساختار رو عوض کنه. یکیش اینکه ما چرا اصلا باید دو تا b داشته باشیم (با فرض اینکه نسخه‌ها یکی باشن)؟ یا اینکه اگه مثلا c هم یک سری دیپندنسی داشت که اونا هم باز دیپندنسی داشتن که اونا هم... تا ابد باید همینطوری فولدر node_modules بسازه و بره جلو؟ درسته فانکشن‌های ریکرسیو خفنن، ولی نه دیگه تا این حد! مخصوصا اینکه اگه یکی از کلاینت‌های ما بخواد ویندوز باشه و حتی نتونه اون فولدرهای داخلی رو به علت طولانی بودن آدرس نشون بده!

بعد از بحث‌های زیاد درباره این قضیه، NPM تصمیم گرفت روشش رو عوض کنه. توی نسخه‌های بعد 3 دیگه NPM سعی می‌کنه تا جایی که راه داره همه چیز رو فلت کنه. الان اگه بخوایم همون وضعیت آخری رو در نظر بگیریم، وقتی با NPM جدید کامند npm i رو اجرا کنیم، ساختار ما این‌شکلی میشه:



---- my app

-------- node_modules

------------ a

------------ b

------------ c

------------ d




به‌به. الان دیگه به جای داشتن یه عالمه node_modules‌ تو در تو فقط یه دونه ازش داریم و همه چیز همینجوری وسطش پهن شدن. اما صبر کنید بذارید وضعیتی رو بگم که ممکنه این ساختار اونقدری هم که بنظر می‌رسه، خوب نباشه.


فرض کنید شما دارید یک پلاگین برای babel می‌نویسید. این ماژول مثلا میاد به یه سری فرایندهای کار babel کمک می‌کنه. حالا می‌خواید این رو منتشرش کنید،

آیا babel باید توی devDependencyها باشه؟ معلومه که نه. چون همیشه به babel نیاز داره تا کار کنه!

فهمیدم، باید توی dependencyها باشه؟ nice try، اما بازم جواب اشتباه بود. این پلاگین به babel نیاز نداره، این babelـه که به این نیاز داره.

پس چی‌کار کنیم؟ به babel بگیم این پلاگین مارو به dependencyهای خودش اضافه کنه؟ نه نه. لایبرری babel نمی‌تونه هر روز بیاد یه عالمه plugin که هر روز دولوپرها براش منتشر می‌کنن رو به dependencyهای خودش اضافه کنه. اصلا منطقی نیست. هرکی babel رو می‌خواد دلیل نمیشه همه pluginهاش رو هم بخواد.


شاید توی بعضی پروژه‌ها و فایل package.jsonـشون قسمت peerDependencies رو دیده باشید. این لایببری‌هایی که توی این بخش نوشته می‌شن، نه موقع develop و نه موقع انتشار نصب نمی‌شن! فقط کارشون اینه که اگه کسی این پلاگین رو توی پروژه mammad نصب کرد، npm در صورتی که تو همون پروژه mammad پکیج babel نصب نبود، به کاربر یه پیغام زرد رنگ که ممکنه خیلی راحت ازش بگذرید بده که «یکی از پکیج‌های شما یه پکیج دیگه رو می‌خواد که شما نداریدش یا یه نسخه دیگه ازش دارید که اون نمی‌تونه نصبش کنه. نصبش کنید!»


این وضعیت ممکنه گند بزنه به پروژه شما. حالت زیر رو در نظر بگیرید: (می‌دونم ممکنه سخت باشه)


// package.json
{
	"name": "plugin",
	"peerDependecies": {
		"babel": "1.0.0"
	}
}

{
	"name": "another-plugin",
	"peerDependecies": {
		"babel": "2.0.0"
	}
}

{
	"name": "custom-builder",
	"dependencies": {
		"babel": "1.0.0",
		"plugin": "latest"
	}
}

{
	"name": "my-app",
	"dependencies": {
		"babel": "2.0.0",
		"another-plugin": "latest",
		"custom-builder": "latest"
	}
}



اوه اوه. پکیج plugin میگه نصب‌کننده‌های من باید babel نسخه یک رو داشته باشن. پکیج custom-builder میگه من نسخه یک babel و plugin رو توی دیپندنسی‌هام می‌خوام. و پکیج my-app که پروژه نهایی ماست، پکیج babel نسخه دو و custom-builder رو توی dependencyهاش داره. اینجا به قول معروف NPM گه‌گیجه می‌گیره. مرحله به مرحله با نصب پکیج‌های مورد نیاز my-app پیش می‌ریم:


- اقدام به نصب پکیج babel نسخه دو، چون توی روت پروژه نسخه دیگه‌ای ازش نیست، همونجا نصب می‌شه.

- اقدام به نصب پکیج another-plugin، چون توی روت پروژه نسخه دیگه‌ای ازش نیست، همونجا نصب می‌شه.

- اقدام به نصب پکیج custom-builder، چون توی روت پروژه نسخه دیگه‌ای ازش نیست، همونجا نصب می‌شه.

- اقدام به نصب dependencyهای custom-builder...

- اقدام به نصب پکیج babel نسخه یک توی روت پروژه. توی روت پروژه نمیشه چون اونجا یه نسخه دیگه ازش هست، پس باید بره توی خود custom-builder.

- اقدام به نصب plugin نسخه یک. چون توی روت پروژه نسخه دیگه‌ای ازش نیست، همونجا نصب می‌شه.


پس نتیجه می‌شه:


  • ---- my app
  • -------- node_modules
  • ------------ babel@2.0.0
  • ------------ another-plugin
  • ------------ custom-builder
  • ---------------- node_modules
  • -------------------- babel@1.0.0
  • ------------ plugin



اینجا یه مشکل بزرگ هست. یه بار دیگه برید بالا به peerDependecyـهای پکیج plugin نگاه کنید. اون babel نسخه یک رو میخواد، اما الان تو این پروژه اگه بخواد babel رو ایمپورت کنه، نسخه دو بهش میرسه، جون نزدیک‌ترین babel بهش نسخه دو هست، نه نسخه یک! و طبیعتا ممکنه توی کارش مشکل بوجود بیاد. تنها کاری که NPM تو این شرایط می‌کنه اینه که موقع نصب پکیج‌های my-app به شما بگه «پکیج plugin نسخه یک babel رو می‌خواد ولی شما نسخه دو رو دارید، برید پکیج babel نسخه یک رو دستی نصب کنید»


مشخصه که ما به این راحتی نمی‌تونیم این کار رو کنیم. چون پروژه my-app ما و همینطور another-plugin طبیعتا babel نسخه دو رو لازم دارن. اینجاست که ممکنه editor رو ببندیم و بریم توی توئیتر به NPM و NodeJs و Javascript فحش بدیم.

مشخصه که اگه NPM یه جوری می‌فهمید پکیج plugin رو توی custom-builder نصب می‌کرد، مشکل حل می‌شد. اما به عنوان یک npm install کننده، نمی‌تونیم چنین کاری کنیم. پس دو تا راه داریم، یکی با دید اینکه ما صاحب پکیج my-app هستیم و اون یکی دیگه با دید اینکه ما صاحب پکیج custom-builder هستیم و می‌خوایم مشکل رو به صورت اصولی حل کنیم.


ما صاحب پکیج my-app هستیم...

ما بعد از نصب پکیج‌های مورد نیاز، می‌تونیم بیل رو برداریم و خودمون این مشکلات peerDependenciesهای پکیج‌هامون رو حل کنیم و پکیج‌های مورد نیازشون رو نصب کنیم تا در نهایت اپ ما کار بکنه. حالا مطمئن بشیم که فایل package-lock.json هم تولید شده و اون رو هم با تغییرات دیگه کامیت می‌کنیم. این فایل برای نفر بعدی که کامند npm i رو اجرا می‌کنه ساخته شده و در واقع توش ساختار فعلی فولدر node_modules ماست. پس از این به بعد هر بار npm install کنیم، مطمئنیم دوباره اون مشکل پیش نمیاد و ساختار دقیقا همین شکلی که الان هست می‌شه. اما یه مشکل بزرگ اینه که این روش فقط وقتی جواب می‌ده که my-app ما یه پروژه نهایی باشه و مطمئن باشیم هیچ‌جوره امکانش نیست که کسی بخواد اون رو به عنوان dependency نصبش کنه. چون این فایل package-lock.json که ما تولید کردیم، فقط زمانی معتبره که کسی مستقیما بخواد داخل این فولدر npm install رو اجرا کنه. حالا فرض دوم رو بخونید.


ما صاحب پکیج custom-builder هستیم...

اینجا ما باید یه فایل lock پرقدرت‌تر بسازیم. یه فایلی که علاوه‌بر اینکه یوزرهای مستقیم ما جدی می‌گیرینش، یوزرهایی که ما رو توی dependency‌هاشون نصب می‌کنن هم جدی بگیرنش. این فایل دوم توی فایلی به نام npm-shrinkwrap.json ذخیره میشه. این فایل به شما تضمین می‌کنه که در هر صورت ساختار node_modules شما تغییر نمی‌کنه، حتی اگر شما یه dependency باشید. مراحل کار تا حد زیادی شبیه مرحله قبله، اما بعد از اتمام کار و قبل از کامیت کردن، یه کار دیگه لازمه. اینکه بیایم این فایل lock دومی رو با کامند npm shrinkwrap تولید کنیم. این کامند باعث میشه محتویات فایل package-lock.json به فایل npm-shrinkwrap.json منتقل بشه.



همین. امیدوارم تونسته باشم به اطلاعاتتون اضافه کنم. درصورتی که بنظرتون مطلب خوبی بود، با اشتراک گذاشتنش من رو خوشحال می‌کنید.