Anatomi pesan error

Ketika kamu menjalankan test suite dengan Jest dan proyekmu menggunakan ES Modules (ESM), kamu bisa jadi berhadapan dengan pesan error yang sangat familiar bagi banyak developer JavaScript modern. Pesan error tersebut muncul secara verbatim sebagai berikut:

SyntaxError: Cannot use import statement outside a module

Atau dalam konteks yang lebih spesifik saat Jest menjalankan file tertentu:

/Users/username/project/src/utils/helper.js:1
import { formatDate } from './date.js';
^^^^^^

SyntaxError: Cannot use import statement outside a module

      at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:1479:14)
      at Object.<anonymous> (src/tests/helper.test.js:3:1)

Dalam bahasa Indonesia, pesan ini bermakna: "Tidak dapat menggunakan pernyataan import di luar modul." Artinya, JavaScript engine (dalam hal ini Node.js yang menjalankan Jest) menemukan sintaks import di sebuah file, tetapi file tersebut tidak diperlakukan sebagai ES Module. Secara default, Node.js menganggap semua file .js sebagai CommonJS module, bukan ES Module. CommonJS menggunakan require() dan module.exports, sedangkan ES Modules menggunakan import dan export. Ketika Jest mencoba mengeksekusi file yang berisi import tanpa konfigurasi yang mengizinkan ESM, engine tidak mengenali sintaks tersebut dan langsung melempar SyntaxError.

Kenapa error ini muncul? Jawaban singkatnya adalah mismatch transformasi. Jest secara bawaan berjalan di atas lingkungan CommonJS. Ketika kamu menulis kode sumber menggunakan import/export (ES Modules), Jest harus "mentransformasi" kode tersebut menjadi CommonJS sebelum menjalankannya. Jika transformasi ini tidak dikonfigurasi dengan benar—misalnya karena Babel tidak disetup, atau transform di jest.config.js di-disable, atau file tertentu di-exclude dari transformasi—maka Jest akan mencoba menjalankan kode ESM mentah-mentah di lingkungan CommonJS, dan inilah yang memicu error.

Pertimbangkan skenario konkret berikut. Kamu punya file sumber:

// src/utils/helper.js
import { formatDate } from './date.js';

Export function getGreeting(name) {
  return `Hello, ${name}! Today is ${formatDate(new Date())}`;
}

Dan file test:

// src/tests/helper.test.js
import { getGreeting } from '../utils/helper.js';

Test('returns greeting with date', () => {
  expect(getGreeting('Budi')).toContain('Hello, Budi');
});

Saat kamu menjalankan npx jest, Jest akan membaca helper.test.js, menemukan import, dan jika tidak ada transformer yang mengonversinya ke require(), error punya muncul. Ini bukan masalah di kode logikamu—kodenya benar secara sintaks—tetapi masalah di layer build pipeline Jest yang tidak tahu harus bagaimana menangani import.


Akar masalah yang paling sering

Ada beberapa root cause yang secara berulang muncul di komunitas ketika error ini terjadi. Berikut lima penyebab paling sering, diurutkan dari yang paling banyak ditemui di lapangan:

1. Tidak ada transformer Babel yang dikonfigurasi untuk Jest. Ini adalah penyebab paling dominan. Jest tidak secara otomatis tahu bahwa file-file proyekmu perlu ditransformasi oleh Babel. Jika kamu tidak punya babel.config.js atau tidak mendaftarkan @babel/preset-env di dalamnya, Jest tidak akan mengonversi import ke require. Banyak developer, terutama yang baru pindah dari CRA (Create React App) ke setup manual, tidak menyadari bahwa CRA sudah mengonfigurasi ini di balik layar, dan ketika mereka setup Jest sendiri, transformer ini absen.

2. Properti transform di jest.config.js di-override atau di-disable. Kadang developer secara eksplisit menulis transform: {} atau menggunakan transformIgnorePatterns yang terlalu agresif sehingga file-file ESM dari node_modules ikut ter-exclude dari transformasi. Ini sangat sering terjadi ketika menggunakan library pihak ketiga yang mengemas kodenya sebagai ESM, seperti got, ky, atau nanoid versi terbaru.

3. Konflik antara jest.config.js dan package.json. Jika kamu punya konfigurasi Jest di kedua file, prioritas merge-nya bisa bikin hasil akhir tidak sesuai harapan. Misalnya, package.json punya "jest": { "transform": {} } yang menimpa konfigurasi jest.config.js tanpa kamu sadari, membuat transformer tidak aktif.

4. Versi Babel dan Jest yang tidak kompatibel. Jest versi 27+ memperkenalkan perubahan di jest-runtime dan jest-transform. Jika kamu menggunakan babel-jest versi lama dengan Jest versi baru, transformer mungkin tidak di-load dengan benar. Begitu juga sebaliknya—Jest versi lama dengan babel-jest versi baru bisa menghasilkan behavior yang tidak terduga.

5. File yang menggunakan ESM di node_modules tanpa konfigurasi transformIgnorePatterns yang tepat. Secara default, Jest tidak mentransformasi file di node_modules. Namun, semakin banyak library modern yang mengemas kode mereka sebagai ESM. Ketika test-mu mengimpor library tersebut, Jest mencoba menjalankan file ESM mentah, dan error punya muncul. Ini adalah penyebab yang sangat sering ditemui di proyek-proyek yang menggunakan dependencies modern.

Cara memastikan penyebab mana yang kamu alami:

  • Jalankan Jest dengan flag --no-cache terlebih dahulu untuk menghilangkan kemungkinan cache stale: npx jest --no-cache. Jika error hilang, masalahnya adalah cache.
  • Periksa apakah Babel terkonfigurasi dengan menjalankan npx babel src/utils/helper.js --out-file /dev/null. Jika Babel error atau tidak ada output transformasi, root cause-nya adalah #1.
  • Cek jest.config.js dan cari properti transform. Jika ada dan nilainya {} atau tidak mencakup js/jsx, root cause-nya adalah #2.
  • Jalankan npx jest --showConfig dan periksa output properti transform dan transformIgnorePatterns di config final. Ini membantu mendeteksi #2, #3, dan #5.
  • Cek versi: npx jest --version dan npx babel --version. Jika major version beda jauh, root cause-nya bisa #4.

Memperbaiki error ini

Bagian ini akan membahas solusi langkah demi langkah untuk setiap root cause yang telah diidentifikasi. Setiap solusi disertai perintah yang harus dijalankan dan output yang diharapkan, sehingga kamu bisa memverifikasi bahwa langkah tersebut berhasil.

Solusi 1: Konfigurasi Babel sebagai transformer Jest

Ini adalah fix untuk root cause #1. Langkah-langkahnya:

Langkah 1: Install dependensi Babel yang diperlukan.

npm install --save-dev @babel/core @babel/preset-env babel-jest

Output yang diharapkan:

added 3 packages in 2s

Langkah 2: Buat file babel.config.js di root proyek jika belum ada.

// babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', {
      targets: {
        node: 'current',
      },
    }],
  ],
};

Mengapa targets: { node: 'current' }? Karena Jest berjalan di Node.js, kita ingin Babel mentransformasi kode ke versi Node.js yang sedang berjalan, bukan ke target browser. Ini menghindari transformasi yang tidak perlu dan membuat test berjalan lebih cepat.

Langkah 3: Pastikan jest.config.js (atau konfigurasi di package.json) memiliki properti transform yang benar. Jika kamu belum punya jest.config.js:

// jest.config.js
module.exports = {
  transform: {
    '^.+\\.jsx?$': 'babel-jest',
  },
};

Jika kamu sudah punya jest.config.js, cukup pastikan properti transform tidak di-override menjadi objek kosong.

Langkah 4: Jalankan test kembali.

npx jest --no-cache

Output yang diharapkan:

 PASS  src/tests/helper.test.js
  ✓ returns greeting with date (3 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total

Verifikasi: Jika test berjalan tanpa SyntaxError, solusi ini berhasil. Kamu juga bisa memverifikasi bahwa Babel benar-benar mentransformasi file dengan menjalankan:

npx babel src/utils/helper.js

Output harus berupa kode CommonJS yang menggunakan require():

"use strict";

Var _date = require("./date.js");

Function getGreeting(name) {
  return "Hello, ".concat(name, "! Today is ").concat((0, _date.formatDate)(new Date()));
}

Exports.getGreeting = getGreeting;

Solusi 2: Fix properti transform yang di-override

Jika kamu menemukan bahwa transform: {} ada di konfigurasi, hapus baris tersebut atau ganti dengan konfigurasi yang benar seperti di Solusi 1. Periksa juga apakah kamu punya duplikat konfigurasi di package.json dan jest.config.js. Hapus salah satunya—best practice-nya adalah gunakan file jest.config.js terpisah, bukan menyimpan konfigurasi di package.json.

Solusi 3: Fix versi Babel-Jest yang tidak kompatibel

Jalankan perintah berikut untuk memastikan babel-jest versinya sesuai dengan Jest:

npm install --save-dev babel-jest@$(npx jest --version | cut -d. -f1)

Atau lebih sederhana, install ulang keduanya secara bersamaan:

npm install --save-dev jest@latest babel-jest@latest @babel/core@latest @babel/preset-env@latest

Solusi 4: Menangani ESM di node_modules

Ini adalah fix untuk root cause #5. Kamu perlu memberitahu Jest untuk mentransformasi library ESM tertentu di node_modules.

// jest.config.js
module.exports = {
  transform: {
    '^.+\\.jsx?$': 'babel-jest',
  },
  transformIgnorePatterns: [
    '/node_modules/(?!library-esm-yang-masalah)',
  ],
};

transformIgnorePatterns default-nya adalah ["/node_modules/", "\\.pnp\\.[^\\/]+$"]. Dengan menimpanya, kamu mengatakan "transform semua file di node_modules kecuali yang tidak cocok dengan pattern baru." Pattern /(?!library-esm-yang-masalah)/ menggunakan negative lookahead, yang artinya "abaikan semua node_modules kecuali library-esm-yang-masalah."

Jika ada beberapa library:

transformIgnorePatterns: [
  '/node_modules/(?!library-a|library-b|library-c)',
],

Setelah mengubah konfigurasi, jalankan:

npx jest --no-cache

Verifikasi umum

Setelah menerapkan solusi apa pun, jalankan tiga perintah berikut untuk memastikan semuanya beres:

# Hapus cache Jest
npx jest --clearCache

# Jalankan test dengan verbose output
npx jest --verbose --no-cache

# Periksa konfigurasi final Jest
npx jest --showConfig | grep -A5 "transform"

Fix permanen (bukan workaround)

Solusi-solusi di section sebelumnya bersifat reaktif—kamu memperbaiki masalah setelah muncul. Bagian ini membahas konfigurasi yang secara permanen mencegah error ini kembali muncul, terlepas dari bagaimana proyekmu berkembang ke depan.

Konfigurasi jest.config.js yang komprehensif

Berikut konfigurasi Jest yang direkomendasikan sebagai fix permanen:

// jest.config.js
module.exports = {
  // Gunakan jsdom untuk test environment jika testing kode browser
  // Gunakan 'node' jika hanya testing kode Node.js
  testEnvironment: 'jsdom',

  // Transform semua .js dan .jsx file menggunakan babel-jest
  transform: {
    '^.+\\.jsx?$': 'babel-jest',
  },

  // Default-nya sudah benar, tapi eksplisit lebih baik
  // Hanya ignore node_modules yang TIDAK mengemas ESM
  transformIgnorePatterns: [
    '/node_modules/(?!(?:library-esm-satu|library-esm-dua)/)',
  ],

  // Pastikan moduleFileExtensions mencakup js
  moduleFileExtensions: ['js', 'jsx', 'json', 'node'],

  // Gunakan moduleNameMapper untuk alias jika diperlukan
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
};

Konfigurasi Babel yang robust

// babel.config.js
module.exports = {
  presets: [
    ['@babel/preset-env', {
      targets: {
        node: 'current',
      },
      // Gunakan modules: 'commonjs' secara eksplisit untuk Jest
      // Ini memastikan output selalu CommonJS
      modules: 'commonjs',
    }],
  ],
  // Hanya apply plugin tertentu di test environment
  env: {
    test: {
      presets: [
        ['@babel/preset-env', {
          targets: {
            node: 'current',
          },
          modules: 'commonjs',
        }],
      ],
    },
  },
};

Mengapa modules: 'commonjs' penting? Secara default, @babel/preset-env akan menghormati konfigurasi type di package.json. Jika package.json punya "type": "module", Babel mungkin tidak mengonversi import ke require() karena menganggap lingkungan mendukung ESM. Dengan mengeksplisitkan modules: 'commonjs', kita memaksa output selalu CommonJS, yang adalah format yang dimengerti Jest secara native.

Best practice jangka panjang

Pertama, selalu gunakan babel.config.js (format JS), bukan .babelrc (format JSON). File babel.config.js secara otomatis berlaku ke seluruh proyek termasuk node_modules yang perlu ditransformasi, sedangkan .babelrc hanya berlaku ke file-file lokal proyekmu. Ini adalah perbedaan kritis yang sering terlewatkan.

Kedua, pertahankan satu sumber truth untuk konfigurasi Jest. Jangan split antara package.json dan jest.config.js. Pilih satu—idealnya jest.config.js—dan hapus properti "jest" dari package.json jika ada. Ini mencegah konflik merge yang merupakan root cause #3.

Ketiga, tambahkan script npm yang membersihkan cache secara rutin:

{
  "scripts": {
    "test": "jest",
    "test:clean": "jest --clearCache && jest --no-cache",
    "test:debug": "jest --no-cache --verbose --showConfig"
  }
}

Keempat, jika proyekmu sepenuhnya ESM dan kamu tidak ingin menggunakan Babel sama sekali, pertimbangkan untuk menggunakan flag --experimental-vm-modules di Node.js:

{
  "scripts": {
    "test": "node --experimental-vm-modules node_modules/.bin/jest"
  }
}

Namun pendekatan ini masih eksperimental dan tidak semua fitur Jest kompatibel. Untuk stabilitas produksi, Babel transformer masih adalah pendekatan yang paling matang.

Perbedaan antar OS

Ada satu perbedaan penting antar OS yang sering menyebabkan masalah: path separator. Di Windows, path menggunakan backslash (\), sedangkan di macOS/Linux menggunakan forward slash (/). Pattern regex di transformIgnorePatterns menggunakan forward slash, dan ini bisa tidak cocok di Windows. Untuk mengatasi ini, selalu gunakan forward slash di pattern—Jest secara internal menangani konversi di Windows. Jangan gunakan path.join() untuk membangun pattern regex karena itu akan menghasilkan backslash di Windows.

Terus, di Windows, perintah node --experimental-vm-modules node_modules/.bin/jest mungkin tidak work karena .bin/jest adalah shell script. Gunakan cross-platform alternative:

{
  "scripts": {
    "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
  }
}

Atau gunakan package cross-env untuk setting environment variable secara cross-platform.


Troubleshooting lanjutan

Kadang, meskipun kamu sudah menerapkan semua solusi di atas, error tetap muncul. Bagian ini membahas skenario-skenario edge case dan cara mendalami masalah.

Skenario 1: Error hanya muncul di CI/CD, tidak di lokal

Ini sangat umum dan biasanya disebabkan oleh perbedaan versi Node.js. CI server mungkin menjalankan Node.js versi yang berbeda dengan mesin lokalmu. Versi Node.js yang lebih lama mungkin tidak mendukung fitur JavaScript tertentu bahkan setelah transformasi Babel. Untuk memverifikasi:

# Di lokal
node --version

# Di CI, tambahkan step di pipeline:
- name: Check Node version
  run: node --version

Pastikan keduanya sama. Gunakan .nvmrc atau engines di package.json untuk mengunci versi:

{
  "engines": {
    "node": ">=18.0.0"
  }
}

Skenario 2: Error muncul hanya pada file tertentu

Jika error hanya muncul pada satu atau beberapa file tertentu, kemungkinan besar file tersebut di-exclude dari transformasi. Periksa apakah file tersebut cocok dengan pattern di transformIgnorePatterns atau tidak cocok dengan pattern di transform. Gunakan --showConfig untuk melihat konfigurasi final:

npx jest --showConfig 2>&1 | less

Cari properti transform dan transformIgnorePatterns, lalu verifikasi bahwa file yang bermasalah tidak di-ignore dan cocok dengan transform pattern.

Kamu juga bisa menggunakan flag --debug untuk mendapatkan output yang lebih detail:

npx jest --debug 2>&1 | grep -i "transform"

Skenario 3: Error muncul setelah upgrade dependency

Saat kamu upgrade Jest atau Babel, konfigurasi yang sebelumnya work bisa jadi tidak kompatibel lagi. Langkah pertama adalah baca changelog versi baru. Langkah kedua adalah reset dan rebuild konfigurasi:

# Hapus semua cache
npx jest --clearCache
rm -rf node_modules/.cache

# Reinstall dependensi
rm -rf node_modules
npm install

# Jalankan test
npx jest --no-cache

Cara baca log dengan efektif

Ketika Jest melempar error, stack trace-nya bisa panjang dan membingungkan. Fokuskan perhatianmu pada tiga bagian:

Pertama, baris paling atas yang berisi SyntaxError itu sendiri. Ini menunjukkan file mana dan baris berapa yang menyebabkan error. Perhatikan path file-nya—jika path mengarah ke node_modules/, masalahnya ada di library pihak ketiga (root cause #5). Jika path mengarah ke file proyekmu sendiri, masalahnya di konfigurasi transform (root cause #1 atau #2).

Kedua, baris yang mengandung at Runtime.createScriptFromCode. Ini menunjukkan bahwa Jest runtime mencoba mengeksekusi kode mentah tanpa transformasi. Ini adalah konfirmasi kuat bahwa transformer tidak bekerja untuk file tersebut.

Ketiga, jika ada pesan seperti Babel compilation failed, itu menunjukkan bahwa transformer ada tapi Babel gagal mentransformasi file—mungkin karena syntax yang tidak dikenali atau plugin Babel yang kurang.

Untuk mendapatkan log yang lebih actionable, jalankan Jest dengan environment variable DEBUG:

DEBUG=jest:* npx jest --no-cache 2>&1 | head -100

Ini akan mencetak log debug internal Jest, termasuk informasi tentang transformer mana yang di-apply ke file mana.

Sumber resmi untuk deep-dive

Jika kamu sudah mencoba semua troubleshooting di atas dan masih bingung, berikut adalah sumber resmi yang bisa kamu jadikan referensi untuk pendalaman lebih lanjut:

  • Dokumentasi resmi Jest tentang ECMAScript Modules: https://jestjs.io/docs/ecmascript-modules — Ini adalah halaman yang wajib dibaca. Dokumen ini menjelaskan secara detail bagaimana Jest menangani ESM, termasuk penggunaan --experimental-vm-modules dan kapan harus menggunakan transformIgnorePatterns.

  • Dokumentasi Jest tentang transform configuration: https://jestjs.io/docs/configuration#transform-objectstring-pathtotransformer--pathtotransformer-object — Menjelaskan cara kerja properti transform dan bagaimana menulis custom transformer.

  • Dokumentasi Babel tentang @babel/preset-env: https://babeljs.io/docs/babel-preset-env — Penting untuk memahami opsi targets dan modules yang sudah kita bahas.

  • GitHub Issues Jest dengan label "ESM": Cari issue dengan label Area: ESM di repository Jest untuk melihat diskusi komunitas dan solusi-solusi edge case yang mungkin belum tercakup di dokumentasi resmi.

  • Node.js documentation tentang ECMAScript Modules: https://nodejs.org/api/esm.html — Untuk memahami bagaimana Node.js sendiri menangani ESM, yang merupakan fondasi dari masalah ini.

Error SyntaxError: Cannot use import statement outside a module di Jest pada dasarnya adalah masalah konfigurasi, bukan masalah kode. Selama kamu memastikan bahwa setiap file yang menggunakan import ditangani oleh transformer yang mengonversinya ke CommonJS, atau bahwa lingkungan Jest dikonfigurasi untuk mendukung ESM secara native, error ini tidak akan pernah muncul lagi. Kunci utamanya adalah memahami pipeline transformasi Jest dan memastikan setiap link di pipeline tersebut berfungsi dengan benar.