Setiap kali kita menulis require("./sapa.cjs") atau import ... from "express", kita sebenarnya hanya menyerahkan satu string ke Node.js — dan Node.js harus mengubah string tersebut menjadi berkas konkret di disk. Proses pencarian itu disebut resolusi module. Memahami algoritmanya membuat pesan error MODULE_NOT_FOUND berhenti terasa abstrak, menjelaskan kenapa node_modules bisa berada di banyak tempat, dan menjadi bekal utama saat kita menyusun package.json pada Chapter berikutnya. Chapter ini menelusuri algoritma tersebut dari specifier paling sederhana sampai pencarian berjenjang antar folder.
Tiga Jenis Specifier #
Algoritma resolusi bermula dari mengelompokkan string yang kita tulis. Node.js mengenal tiga jenis specifier, dan masing-masing menempuh jalur pencarian yang berbeda:
| Specifier | Contoh | Dicari di mana |
|---|---|---|
| Relatif | ./sapa.mjs, ../lib/utama.cjs | Relatif terhadap folder berkas pengimpor |
| Bare | express, sapa-kit | Folder node_modules berjenjang ke atas |
| Core | node:fs, node:path | Di dalam Node.js sendiri, tanpa pencarian berkas |
Specifier relatif selalu diawali ./ atau ../. Tanpa awalan tersebut, Node.js menganggap string sebagai bare specifier — jadi require("sapa.cjs") dan require("./sapa.cjs") adalah dua hal yang sama sekali berbeda: yang pertama dicari di node_modules, yang kedua dicari di sebelah berkas pemanggil. Specifier core memakai prefix node: dan langsung merujuk module bawaan runtime tanpa menyentuh disk.
Resolusi Path Relatif #
Path relatif diselesaikan terhadap lokasi berkas yang menulis require() atau import — bukan terhadap folder tempat perintah node dijalankan. Setelah path dasar ketemu, require() masih melakukan satu langkah tambahan: menebak ekstensi. Urutan tebakannya adalah nama persis, lalu .js, lalu .json, lalu .node (berkas binary khusus). Kita buktikan dengan dua berkas pendamping:
module.exports = { batasTugasHarian: 10 };{
"namaDaftar": "Tugas Pekan Ini",
"jumlahMaksimal": 10,
"temaWarna": "biru"
}Keduanya dimuat tanpa menyebut ekstensi sama sekali:
const konstanta = require("./konstanta");
const data = require("./data");
console.log("konstanta.js dimuat, batas:", konstanta.batasTugasHarian);
console.log("data.json dimuat, nama daftar:", data.namaDaftar);Output yang Dihasilkan:
konstanta.js dimuat, batas: 10
data.json dimuat, nama daftar: Tugas Pekan IniBaris require("./konstanta") gagal menemukan berkas bernama konstanta persis, lalu berhasil pada tebakan kedua: konstanta.js. Begitu berkas ditemukan, isi .js dievaluasi sebagai CommonJS dan module.exports dikembalikan. Baris require("./data") menemukan data.json pada tebakan ketiga — dan require() memperlakukan JSON secara istimewa: alih-alih dieksekusi, berkas di-parsing langsung menjadi object JavaScript.
Tebakan ekstensi tersebut tidak mencakup .cjs. Berkas matematika.cjs memang ada di bundle chapter ini, tetapi specifier tanpa ekstensi berikut gagal menemukannya:
try {
const m = require("./matematika");
console.log("dimuat:", typeof m.tambah);
} catch (error) {
console.log(`${error.code}: ${error.message.split("\n")[0]}`);
}Output yang Dihasilkan:
MODULE_NOT_FOUND: Cannot find module './matematika'Module Inti dan Prefiks node: #
Core module adalah module yang dikompilasi bersama Node.js: fs, path, http, url, dan puluhan lainnya. Module tersebut tidak membutuhkan instalasi dan tidak pernah dicari di node_modules:
const path = require("node:path");
console.log("tipe path.join:", typeof path.join);
console.log(path.join("folder", "berkas.txt"));Output yang Dihasilkan:
tipe path.join: function
folder/berkas.txtPrefix node: memberi dua jaminan. Pertama, specifier node:path pasti merujuk core module — tidak akan pernah tertukar dengan package bernama path yang kebetulan terpasang di node_modules, karena nama core module selalu menang atas nama package ketika keduanya bertabrakan. Kedua, core module yang tidak ada terdeteksi seketika: require("node:modul-tidak-ada") langsung gagal dengan error ERR_UNKNOWN_BUILTIN_MODULE, bukan malah mencari package bernama node:modul-tidak-ada. Konvensi Course ini selalu memakai prefix node: agar niat kode terbaca jelas.
Pencarian node_modules untuk Specifier Bare #
Specifier bare seperti "express" adalah cara kita memanggil package. Node.js tidak memiliki satu lokasi global; alih-alih, pencarian berjalan naik folder demi folder dari lokasi berkas pengimpor. Pada setiap tingkat, Node.js memeriksa folder node_modules di sana:
proyek/
├── node_modules/ ← dicari pada langkah kedua
└── src/
├── node_modules/ ← dicari pada langkah pertama
└── aplikasi.cjs ← berkas pengimpor, titik awal pencarianPencarian berhenti pada node_modules pertama yang memuat package bersangkutan; bila sampai akar filesystem tidak ketemu, error MODULE_NOT_FOUND muncul. Kita buktikan dengan package kecil sapa-kit yang dipasang langsung di bundle chapter ini:
{
"name": "sapa-kit",
"version": "1.0.0",
"description": "Package contoh untuk demontrasi resolusi module",
"main": "lib/utama.cjs"
}Field main menunjuk berkas pintu masuk package. Berkas berikut memuat package tersebut dan menelusuri balik lokasi persisnya lewat require.resolve():
const path = require("node:path");
const sapaKit = require("sapa-kit");
console.log(sapaKit.sapa("Budi"));
console.log("ditemukan di:", path.relative(process.cwd(), require.resolve("sapa-kit")));Output yang Dihasilkan:
Halo dari sapa-kit, Budi!
ditemukan di: node_modules/sapa-kit/lib/utama.cjsFunction require.resolve() menjalankan algoritma resolusi tanpa memuat module — hasilnya path berkas yang akan dieksekusi. Karena require.resolve("sapa-kit") mengembalikan path absolut yang panjang, contoh di atas memangkasnya dengan path.relative() agar output mudah dibaca: package ditemukan di node_modules/sapa-kit/lib/utama.cjs, tepat seperti isi field main.
Berkas demonstrasi berikut memperlihatkan sisi lain algoritma — pendakian ke atas. Folder chapter ini sengaja tidak memiliki node_modules sendiri, tetapi chapter berada di dalam proyek besar yang menyimpan package express di node_modules pada akar proyek:
const path = require("node:path");
const lokasi = require.resolve("express");
console.log(path.relative(process.cwd(), lokasi));Output yang Dihasilkan:
../../../../node_modules/express/index.jsEmpat kali ../ pada output adalah jejak pendakian algoritma: dari folder chapter ini, Node.js naik empat tingkat sampai menemukan node_modules yang memuat express. Inilah sebabnya satu proyek hanya perlu satu node_modules di akar — berkas di kedalaman src/ mana pun tetap menemukan package yang sama tanpa perlu salinan di setiap folder.
Ketika Module Tidak Ditemukan #
Semua jalur pencarian yang gagal berujung pada error, dan bentuk errornya berbeda antara CommonJS dan ESM. Pada CommonJS, error membawa kode MODULE_NOT_FOUND:
try {
require("./tidak-ada.cjs");
} catch (error) {
console.log(`${error.code}: ${error.message.split("\n")[0]}`);
}Output yang Dihasilkan:
MODULE_NOT_FOUND: Cannot find module './tidak-ada.cjs'Pesan lengkap error tersebut sebenarnya masih memuat require stack berisi path absolut setiap berkas pemanggil — contoh di atas sengaja memanggil error.code dan baris pertama error.message agar output tetap ringkas dan portabel. Pada ESM, kode errornya ERR_MODULE_NOT_FOUND dan pesannya memuat berkas pengimpor:
try {
await import("./tidak-ada.mjs");
} catch (error) {
console.log(error.code);
console.log(error.message.replaceAll(process.cwd() + "/", ""));
}
try {
await import("./konstanta");
} catch (error) {
console.log(error.code);
}Output yang Dihasilkan:
ERR_MODULE_NOT_FOUND
Cannot find module 'tidak-ada.mjs' imported from modul-hilang.mjs
ERR_MODULE_NOT_FOUNDBlok try kedua adalah bukti penting perbedaan kedua sistem: berkas konstanta.js nyata ada di bundle, require("./konstanta") berhasil memuatnya, tetapi import("./konstanta") tetap gagal karena resolver ESM tidak menebak ekstensi .js. Setiap import ESM harus menuliskan nama berkas secara lengkap — .mjs, .js, atau .json beserta atribut with — sejak awal.
Algoritma resolusi kini tuntas dari specifier sampai berkas. Satu benda telah berulang kali muncul di sepanjang jalur pencarian: package.json — sebagai penentu format .js di sebuah folder, penentu pintu masuk package lewat main, dan tempat dependencies dicatat. Saatnya kita bedah berkas tersebut satu field demi field pada Chapter package.json.