Pada sekitar tahun 2009, ada sebuah teknologi baru bernama Node.js, sebuah runtime untuk menjalankan kode JavaScript di bagian peladen/server.

Masih di tahun yang sama, pada 1 oktober Isaac memperkenalkan sebuah teknologi bernama npm, singkatan dari Node Package Manager.

Biasanya JavaScript dijalankan di lingkungan klien/peramban, untuk membuat halaman situs menjadi lebih interaktif, daripada hanya menjadi lebih cantik dengan bantuan teknologi CSS.

Sekarang, hampir dimana-mana JavaScript dijalankan. Di peladen, di peramban, di perangkat genggam, dan mungkin di tamagoci.

JavaScript is eating the world, ucap Kevin Lacker, mantan CTO Parse yang sudah diakuisisi oleh Facebook pada sekitar tahun 2013. JavaScript berhasil merambah ke berbagai platform bekerja di berbagai industri, dan di berbagai skala.

Dan pada sekitar tahun 2015, Brendan Eich (pembuat JavaScript) di talk nya menegaskan bahwa Always bet on JS yang bisa kita lihat bagaimana perkembangannya sampai sekarang.

Karena sifat JavaScript yang dapat berjalan di lintas platform inilah terkadang ada beberapa hal yang menyebabkan keliru. Yang paling kontras adalah pemaknaan Node.js itu sendiri yang dianggap sebagai Bahasa Pemrograman dibanding hanya sebagai JavaScript Runtime.

Selain itu, juga ada beberapa sedikit kesalahpahaman, salah satunya ketika melakukan permintaan ke peladen secara asinkron yang biasa kita gunakan di peramban menggunakan metoda fetch yang padahal API (native) Fetch hanya berada di lingkungan peramban.

Oke oke, tapi disini kita sedang tidak membahas tentang JavaScript lebih dalam melainkan hanya sebatas pengelola dependensinya saja.

Namun sebelum kita membahas lebih lanjut tentang itu, mari kita bahas sedikit tentang perjalanan yang membuat kita—pengembang web—bisa sampai sini, menggunakan npm sebagai bagian dari alur kerja kita namun subjektif berdasarkan pengalaman penulis.

Perjalanan menggunakan dependensi

Ketika membuat module di JavaScript yang dapat digunakan oleh pihak ketiga, setidaknya ada 3 pola yang bisa kita gunakan dalam merancang module tersebut: AMD, UMD, dan CommonJS.

Pola apapun yang kita gunakan, intinya agar module tersebut dapat dikonsumsi oleh kode kita dan terdaftar di namespace yang dapat kode kita akses.

Yang paling umum digunakan adalah jQuery, sebuah helper untuk menulis kode JavaScript menjadi lebih singkat.

jQuery menawarkan namespace $ yang sederhananya, ketika kita menggunakan $ seperti $(element).click(fn), pada dasarnya kita memanggil fungsi dari jQuery untuk memilih element yang kita maksud; mendefinisikan ada event apa terhadap element tersebut (click), lalu memanggil fungsi yang sudah didefinisikan ketika element tersebut menerima event click.

Untuk memuatnya, biasanya pengembang module menawarkan cara memuat melalui CDN atau pengembang bisa mengunduh kode tersebut, dan menyimpannya di project directory mereka.

Alur kerja diatas terlihat efektif, sampai menjadi tidak.

Jika biasanya yang sering diagungkan biaya dari menggunakan dependensi adalah di bundle dan install size, namun ada biaya tersembunyinya, yakni di pemeliharaan (maintainability).

Mengatur versi yang digunakan adalah hal yang tidak sederhana.

Anggap kita menggunakan jQuery versi 1.12.1, lalu ingin menggunakan plugin dengan nama jquery-carousel yang mendukung jQuery versi 2.2.4. Apa yang kita lakukan? Mencari alternatif lain? Sayangnya, biasanya, kita memilih untuk meningkatkan versi jQuery kita karena yang penting plugin tersebut bisa berjalan.

Dan berharap tidak ada yang tidak berjalan.

Proses peningkatan versi tersebut cenderung dilakukan secara imperatif, pengembang mengubah apapun yang terkait dengan versi (misal menggunakan find+replace) langsung ke berkas yang menggunakannya.

Jika tidak menggunakan CDN, mungkin dengan cara mengunduh bundle dari module tersebut dari situs nya, untar, diakhiri dengan copy-paste ke project kita.

Lalu hadirlah Bower, a package manager for the web.

Bower memudahkan pengembang untuk mengelola module pihak ketiga yang digunakan, dengan menggunakan alur kerja yang kurang lebih seperti yang kita gunakan sekarang, namun menggunakan kata kunci npm daripada bower.

Seiring berjalan waktu, sampailah kita ke masa dimana JavaScript digunakan untuk melakukan hal-hal terkait build. Jika sebelumnya kita menggunakan Java untuk bisa menjalankan Google Closure Compiler yang berguna untuk melakukan hal-hal terkait minification, sekarang kita memiliki gulp, grunt, dan brunch yang kurang lebih melakukan apa yang dilakukan oleh Closure Compiler juga, namun ditulis menggunakan JavaScript dan mendukung sistem plugin agar lebih fleksibel terkait kebutuhan khusus pengembang.

Setelah masa-masa Task Runner, sampailah kita di masa Module Bundler.

Teknologi-teknologi seperti Webpack, Rollup, Parcel, dan lain sebagainya yang menamakan diri mereka sebagai Module Bundler sedang berjaya-jayanya, mengingat alur kerja pengembang web sekarang menggunakan pendekatan komponen (daripada template) dalam membuat antarmuka pengguna.

Dan sampai hari ini, kita masih berada pada masa tersebut.

Dengan bantuan module bundler, kita dapat membuat antarmuka pengguna dengan pendekatan komponen; sambil menulisnya dengan sintaks JavaScript yang lebih modern, dengan hasil keluaran yang bisa ter-optimasi sekaligus dapat digunakan oleh pengguna akhir, sekalipun kita menulis tag yang seperti HTML di kode JavaScript.

Berkenalan dengan dependensi

Dalam lingkungan Node.js, dependensi terbagi menjadi tiga:

  • Dependensi yang digunakan di runtime juga
  • Dependensi yang hanya digunakan di development
  • Dependensi yang seharusnya digunakan oleh host

Mungkin poin 1 dan 2 sudah lumayan familiar, untuk poin 3, mari kita ilustrasikan.

Anggap kita membuat plugin untuk menampilkan modal di jQuery. Ketika kita mengembangkan plugin tersebut, kita membutuhkan jQuery dalam proses pengembangannya.

Namun ketika plugin tersebut dikonsumsi, tentu kita akan menggunakan jQuery yang sudah di pasang oleh konsumen (pengembang) daripada menggunakan jQuery yang ada di plugin tersebut.

Kenapa?

Pertama, untuk menghindari kemungkinan konflik karena berbeda versi yang digunakan.

Kedua, karena module/plugin tersebut dikhususkan untuk jQuery, asumsinya pengembang sudah menggunakan jQuery di project nya.

Ketiga, untuk mengurangi error prone. Konflik terkait perbedaan versi bukan hanya tentang kompatibilitas, namun untuk menghindari terjadinya tingkah laku yang tidak diharapkan.

Nah, jenis dependensi ini disebut peerDependencies, sebuah daftar dependensi yang seharusnya sudah dipasang oleh konsumen dari dependensi tersebut.

dependencies

Jika belum terlalu familiar dengan dependencies, daftar dependensi yang berada disini akan dipasang baik itu di lingkungan development ataupun production. Jika di development dan production kita membutuhkan React, maka kita masukkan React ke dependencies.

Jika kamu menggunakan npm install atau yarn add tanpa parameter apapun, by default dependensi yang dipasang akan masu ke kategori dependencies.

devDependencies

Ini jenis dependensi yang sebenarnya hanya dibutuhkan ketika development.

Ambil Webpack sebagai contoh, di production kita tidak membutuhkan Webpack karena yaa meh naon.

Ketika di CI kita menggunakan npm install --production, maka di node_modules kita tidak akan ada dependensi-dependensi yang berada di devDependencies, berguna untuk menghemat ruang penyimpanan peladen kita.

Untuk memasang dependensi yang hanya dibutuhkan ketika development, kita harus eksplisit memberitahukan package manager yang kita gunakan dengan npm install <module> --only=dev bila menggunakan npm dan yarn add <module> --dev bila menggunakan yarn.

peerDependencies

Ini yang sudah kita bahas sedikit sebelumnya, intinya untuk menandakan bahwa dependensi tersebut seharusnya sudah dipasang juga di host dan seharusnya menggunakan versi yang sama.

Untuk memasang dependensi yang dianggap sebagai peerDependencies, kita bisa menggunakan yarn dengan perintah yarn add <module> --peer yang sayangnya tidak bisa dilakukan di npm versi ^3.0.0 yang kurang jelas dan entah berpengaruh terhadap behavior aplikasi kita atau tidak dalam pemasangan dependensi.

Struktur dependensi

node_modules memiliki struktur flat, yang sederhananya bila kita membutuhkan dependensi fariz, dan dependensi fariz membutuhkan dependensi rizaldy, maka struktur direktorinya menjadi:

node_modules/
├── fariz
|
└── rizaldy

Bukan:

node_modules/
└── fariz
    └── node_modules
       └── rizaldy

Namun beda cerita bila misal kita menambah dependensi lain dengan nama rin, dependensi tersebut membutuhkan rizaldy juga misalnya, namun dengan versi 6.6.6, maka strukturnya akan menjadi seperti ini:

node_modules/
├── fariz
|
├── rizaldy@1.0.0
|
└── rin
    └── node_modules
       └── rizaldy@6.6.6

Ini layak diketahui khususnya ketika kita membuat module juga, namun diluar project utama kita.

Jika kamu menggunakan react-scripts, script tersebut memiliki "upacara" bernama preflight check, dan masalah yang umum terjadi adalah ketika dependensi yang digunakan oleh react-scripts tidak sesuai versinya dengan yang kita gunakan.

Misal, kita menggunakan react-scripts dan next juga.

react-scripts membutuhkan Webpack versi 4.42.0 sedangkan next yang versi 4.44.2.

Tergantung siapa dulu yang dipasang, bila next terlebih dahulu, maka versi webpack yang ada di top-level node_modules seharusnya adalah 4.44.2.

Begitupula bila sebaliknya.

Apakah ada dampaknya?

react-scripts preflight check

Tangkapan layar diatas adalah contoh untuk kasus react-scripts dengan masalah versi webpack yang ada di top-level node_modules adalah 4.44.2, lalu ketika menjalankan perintah react-scripts, modul tersebut menggunakan webpack versi 4.44.2 yang mana tidak sesuai dengan yang diekspektasikan (4.20.0) yang meskipun jika melihat ke aturan Semantic Versioning seharusnya tidak ada masalah mengingat versi major nya sama.

...ya kecuali kalau terkait fitur.

Tapi seharusnya bukan tanpa alasan mengapa react-scripts melakukan "preflight check" tersebut, kan?

Mengapa ini layak untuk diketahui?

Ketika kita mempertimbangkan untuk bergantung dengan modul yang dikembangkan dan dipelihara oleh pihak ketiga, kita harus mempertimbangkan juga kemungkinan dampak yang terjadi terhadap alur kerja kita sekarang.

Siapa yang tahu bila ketika kita memasang modul @evilfactorylabs/bpjs ternyata module tersebut melakukan pengiriman berkas yang memiliki nama /sex/i ke random server.

Tidak ada yang tahu, kan? Emang siapa sih yang memerika sumber kode sebelum menjadikan modul tersebut sebagai dependensi di project kita?

Jika berbicara spesifik tentang topik ini, salah satunya adalah tentang bagaimana struktur direktori terbentuk, bagaimana bedanya tingkah laku dari npm dan yarn dalam menyusun struktur tersebut, dan tentang bagaimana kemungkinan dampak dari pengaruh menggunakan dependencies, devDependencies, dan peerDependencies.

Penutup

Semenjak kita sudah jarang menggunakan <script src='https://some-random-cdn/some-random-library.min.js'> ketika menggunakan modul dari pihak ketiga, sepertinya pengetahuan dasar tentang bagaimana package manager mengelola package tersebut bekerja layak untuk diketahui.

Juga, sepertinya berguna ketika kita akan mengembangkan modul sendiri khususnya ketika modul tersebut akan dikonsumsi juga oleh pihak ketiga.

Di tulisan ini kita hanya membahas sebatas Package Manager, mungkin di tulisan selanjutnya kita akan membahas tentang beragam format/pola dalam membuat modul seperti CommonJS, UMD, ESM, dan mungkin WASM.

Terima kasih sudah mampir!