Source map adalah file .map yang mencatat, untuk posisi-posisi di file JavaScript hasil build, di file mana, baris berapa, dan kolom berapa kode itu aslinya kamu tulis. Bayangin novel yang dicetak ulang jadi satu halaman panjang tanpa paragraf supaya hemat kertas. Pembaca yang menemukan typo cuma bisa bilang "halaman 10, huruf ke-25". Source map itu lembar contekan penerbit yang menerjemahkan posisi itu jadi "bab App, baris 42, huruf ke-10" di naskah aslinya.
🧪 Tebakan cepat: file
bundled.js.mapikut ter-upload ke server dan bisa dibuka siapa saja. Apa yang bisa didapat orang lain dari file itu: cuma daftar nama file, atau seluruh kode aslimu? Jawabannya ada di bagian "Source map di production".
Kenapa stack trace production gak bisa dibaca
Sebelum di-deploy, kode kamu diproses bundler (Vite, webpack, esbuild): banyak file digabung jadi satu (bundle), lalu di-minify, yaitu spasi dan komentar dibuang dan nama variabel dipendekkan. Ini dua potongan kode asli:
// src/App.tsx, baris 40 sampai 43
export function sapaan(user: User) {
return "Hai kak " +
user.name +
", apa kabarnya";
// src/utils.ts, baris 6 sampai 9
export function tiapItem<T>(list: T[], fn: (item: T) => void) {
for (const item of list) {
fn(item);
}
Di bundled.js, keduanya jadi satu baris, baris ke-10:
const r=t=>"Hai kak "+t.name+", apa kabarnya";function o(t,n){for(const e of t)n(e)}
Waktu tiapItem dipanggil dengan daftar yang isinya undefined, Chrome mencetak stack trace ini (alamat lengkap file-nya disingkat jadi bundled.js):
TypeError: Cannot read properties of undefined (reading 'name')
at r (bundled.js:10:25)
at o (bundled.js:10:80)
at bundled.js:99:1
Formatnya file:baris:kolom, dan stack trace menghitung baris dan kolom mulai dari 1. Kolom 25 di baris 10 adalah name di t.name, dan kolom 80 adalah n(e). Tapi r, o, dan bundled.js gak bilang apa-apa soal file aslimu.
Isi file .map
Bundler bisa ikut membuat bundled.js.map, sebuah file JSON:
{
"version": 3,
"file": "bundled.js",
"sources": ["src/App.tsx", "src/utils.ts"],
"sourcesContent": ["...isi lengkap App.tsx...", "...isi lengkap utils.ts..."],
"names": [],
"mappings": ";;;;;;;;;wBAyCS,uDClCL"
}
sources: daftar file asli. Index 0 adalahsrc/App.tsx, index 1src/utils.ts.sourcesContent: isi lengkap file-file aslinya, supaya DevTools bisa menampilkannya.mappings: tiap;memisahkan baris bundle, dan tiap,memisahkan segmen dalam satu baris. Ada 9 titik koma sebelumwBAyCS, jadi dua segmen itu milik baris ke-10.- Baris terakhir bundle berisi komentar
//# sourceMappingURL=bundled.js.map, supaya browser tahu letak file.map-nya.
Tiap segmen menyimpan empat angka dalam kode Base64 VLQ (angka yang ditulis pakai huruf supaya ringkas): kolom di bundle, index file asli, baris asli, dan kolom asli. Kalau di-decode dan dihitung mulai dari 1, isinya dua mapping ini:
| Posisi di bundle | File asli | Baris | Kolom |
|---|---|---|---|
| baris 10, kolom 25 | src/App.tsx |
42 | 10 |
| baris 10, kolom 80 | src/utils.ts |
8 | 5 |
Mencocokkan posisi (yang dikerjakan latihan)
Di latihan, mappings sudah di-decode jadi array seperti tabel tadi. Tugasmu mencari posisi asli dari posisi bundle:
function createSourceMapResolver(mappings) {
let unmapped = 0;
function resolve(line, column) {
const cocok = mappings.find((m) => m.bundled.line === line && m.bundled.column === column);
if (!cocok) {
unmapped++; // dihitung di sini saja, sekali per pencarian yang gagal
return null;
}
return cocok.original; // { file, line, column }
}
return {
resolve,
resolveFrame(frame) {
const [file, line, column] = frame.split(':'); // "bundled.js:10:80"
const asli = resolve(Number(line), Number(column));
if (!asli) return frame; // gak ada mapping: kembalikan apa adanya
return asli.file + ':' + asli.line + ':' + asli.column;
},
unmappedCount: () => unmapped,
};
}
const r = createSourceMapResolver([
{ bundled: { line: 10, column: 25 }, original: { file: 'src/App.tsx', line: 42, column: 10 } },
{ bundled: { line: 10, column: 80 }, original: { file: 'src/utils.ts', line: 8, column: 5 } },
]);
const r1 = r.resolve(10, 25);
console.log(r1.file); // src/App.tsx
console.log(r1.line); // 42
console.log(r.resolveFrame('bundled.js:10:80')); // src/utils.ts:8:5
console.log(r.resolveFrame('bundled.js:99:1')); // bundled.js:99:1
console.log(r.resolve(5, 5)); // null
console.log(r.unmappedCount()); // 2
resolvemencari mapping yang baris dan kolom bundle-nya cocok persis, lalu mengembalikanoriginal, ataunullkalau gak ada.frame.split(':')memecah"bundled.js:10:80"jadi["bundled.js", "10", "80"].Number(...)mengubah teks jadi angka, karena"10" === 10itufalse.bundled.js:99:1(kode pemanggil di baris 99) gak punya mapping, jadi dikembalikan utuh, bukannulldan bukan string kosong.unmappedcuma naik di dalamresolve.resolveFramememanggilresolve, jadi kalau kamu menaikkannya di dua tempat, satu frame gagal terhitung dua kali. Di contoh ini yang gagal ada dua:99:1dan(5, 5).
Hasilnya, stack trace tadi jadi at src/App.tsx:42:10, at src/utils.ts:8:5, dan at bundled.js:99:1. Baris 42 di App.tsx adalah user.name: user-nya undefined.
Di latihan pencocokannya persis. DevTools dan Sentry sedikit lebih longgar: mereka memakai segmen terdekat di sebelah kiri pada baris yang sama, jadi kolom 30 pun tetap jatuh ke src/App.tsx:42:10.
Source map di production
Source map berisi sourcesContent, yaitu seluruh kode aslimu, lengkap dengan nama variabel dan komentar. Kalau file .map bisa diunduh publik, siapa saja bisa membaca kodemu. Itu jawaban tebakannya. Di vite.config.js, atur build.sourcemap:
sourcemap: truemembuat.mapdan menambahkan komentarsourceMappingURL. Cocok untuk kode yang memang publik.sourcemap: 'hidden'membuat.maptanpa komentar itu. Upload.map-nya ke error tracker (Sentry punya plugin Vite dansentry-cli), lalu jangan ikut sajikan ke publik. Sentry yang menerjemahkan stack trace-nya.- Di webpack, padanannya
devtool: 'source-map'dandevtool: 'hidden-source-map'.
Di Chrome, source map dipakai otomatis kalau Settings → Preferences → Sources → "Enable JavaScript source maps" menyala (bawaannya menyala) dan file .map-nya bisa diambil. Panel Sources lalu menampilkan src/App.tsx asli, dan breakpoint bisa dipasang di situ.
💡 Membaca satu segmen VLQ
Segmen wBAyCS di-decode per huruf. Tiap huruf Base64 bernilai 0 sampai 63 (A = 0, a = 26, w = 48). Bit bernilai 32 artinya "angkanya masih berlanjut ke huruf berikutnya", 5 bit sisanya adalah isinya, dan bit paling kecil dari angka jadinya adalah tanda minus.
| Huruf | Nilai | Lanjut? | Angka jadi |
|---|---|---|---|
w B |
48, 1 | ya, tidak | 16 + 1 × 32 = 48, bit kecil 0, jadi 48 / 2 = 24 |
A |
0 | tidak | 0 |
y C |
50, 2 | ya, tidak | 18 + 2 × 32 = 82, jadi 41 |
S |
18 | tidak | 18, jadi 9 |
Hasilnya [24, 0, 41, 9]: kolom 24, file index 0, baris 41, kolom 9, semuanya dihitung dari 0. Ditambah 1, jadi kolom 25 di bundle sama dengan src/App.tsx:42:10. Segmen berikutnya, uDClCL, berisi [55, 1, -34, -5]: angka-angka itu selisih dari segmen sebelumnya, jadi kolomnya 24 + 55 = 79, filenya index 1, barisnya 41 - 34 = 7, kolomnya 9 - 5 = 4.
⚠️ Jebakan yang sering ketemu
.mapikut tersaji publik. Kodemu bisa dibaca utuh. Pakaisourcemap: 'hidden', upload ke error tracker, dan jangan deploy file.map-nya.- Map dari build yang lain. Source map cuma cocok dengan bundle yang dibuat bersamanya. Kalau yang di-upload map dari build lama, posisinya menunjuk ke baris yang salah, tanpa error apa pun.
- Hitungan dari 0 atau dari 1. Stack trace menghitung baris dan kolom dari 1, isi
mappingsdari 0. - Di latihan: frame tanpa mapping.
resolveFrame('bundled.js:99:1')harus mengembalikan"bundled.js:99:1", danunmappedCount()harus2, bukan3: naikkan penghitungnya diresolvesaja.
TL;DR: Source map (.map) memetakan posisi di bundle yang sudah di-minify ke file, baris, dan kolom aslinya. Stack trace bundled.js:10:25 lewat mapping jadi src/App.tsx:42:10. Isi mappings adalah segmen Base64 VLQ, dipisah ; per baris bundle dan , per segmen, dihitung dari 0. Di latihan: resolve mencari yang cocok persis atau null, resolveFrame memecah file:baris:kolom dan mengembalikan frame aslinya kalau gak ketemu, dan unmappedCount menghitung tiap pencarian yang gagal sekali. Di production pakai sourcemap: 'hidden' dan upload ke error tracker, karena .map publik berarti seluruh kode aslimu ikut terbaca. Di Chrome, nyalakan "Enable JavaScript source maps".