Perkakas Build

Peran esbuild, Vite, dan tsx serta batas tanggung jawab tsc di dalamnya.

Daftar isi

Compiler tsc bukan satu-satunya alat yang menyentuh berkas TypeScript pada proyek nyata. Topik ini membahas tiga perkakas yang paling sering dipakai di sekitarnya, yaitu esbuild untuk transformasi dan penggabungan berkas, Vite untuk server pengembangan dan build aplikasi, serta tsx untuk mengeksekusi berkas TypeScript secara langsung. Pembahasan menempatkan tsc pada posisi yang sesungguhnya, yaitu pemeriksa tipe yang berdiri terpisah dari proses build. Setelah membacanya, pembagian tugas antara alat build dan pemeriksa tipe dapat disusun dengan jelas pada proyek apa pun.

Transformasi dan penggabungan dengan esbuild#

esbuild membaca berkas TypeScript, membuang seluruh anotasinya, lalu menghasilkan JavaScript yang dapat dijalankan browser maupun Node.js. Proses ini berjalan sangat cepat karena esbuild ditulis dalam Go dan tidak melakukan satu pun pemeriksaan tipe. Mode pertamanya adalah transform, yaitu mengubah satu berkas sumber menjadi satu berkas keluaran, yang dapat dicoba melalui input standar terminal. Berkas hitung-kata.ts di bawah ini menjadi bahan ujinya.

ts hitung-kata.ts
export interface OpsiHitung {
  abaikanKapital: boolean;
}

export function hitungKata(teks: string, opsi: OpsiHitung): number {
  const bersih = opsi.abaikanKapital ? teks.toLowerCase() : teks;
  const potongan = bersih.trim().split(/\s+/).filter((k) => k.length > 0);
  return potongan.length;
}
bash
# mengubah berkas TypeScript menjadi JavaScript melalui input standar
esbuild --loader=ts < hitung-kata.ts
text Keluaran
export function hitungKata(teks, opsi) {
  const bersih = opsi.abaikanKapital ? teks.toLowerCase() : teks;
  const potongan = bersih.trim().split(/\s+/).filter((k) => k.length > 0);
  return potongan.length;
}

Keluarannya memperlihatkan pekerjaan esbuild secara harfiah. Interface OpsiHitung lenyap seluruhnya, anotasi tipe pada parameter dan kembalian hilang, sedangkan isi fungsi tetap utuh apa adanya. Mode kedua esbuild adalah bundle, yaitu mengikuti pernyataan import dari satu berkas masuk lalu menggabungkan seluruh modul yang dirujuknya menjadi satu berkas keluaran. Berkas laporan-kata.ts memakai modul tersebut dan dapat dijalankan langsung maupun melalui hasil bundle-nya.

ts laporan-kata.ts
// tidak-dijalankan
import { hitungKata, type OpsiHitung } from "./hitung-kata.ts";

const opsi: OpsiHitung = { abaikanKapital: true };
const catatan = "Belajar modul ES dan perkakas build";
console.log(hitungKata(catatan, opsi), "kata");
bash
# menjalankan berkas sumbernya secara langsung
node --no-warnings laporan-kata.ts
text Keluaran
6 kata
bash
# membundle kedua berkas di folder sementara lalu menjalankan hasilnya
rm -rf /tmp/esb-demo && mkdir -p /tmp/esb-demo
cp hitung-kata.ts laporan-kata.ts /tmp/esb-demo/
( cd /tmp/esb-demo && esbuild laporan-kata.ts --bundle --outfile=bundle.js --log-level=silent && node bundle.js )
text Keluaran
6 kata

Keluaran yang sama dicapai melalui dua jalur yang berbeda, yaitu eksekusi langsung berkas sumber dan eksekusi hasil bundle. Perintah --log-level=silent hanya meredam laporan kemajuan esbuild agar keluaran programnya terbaca bersih. Pada proyek nyata, berkas keluaran seperti bundle.js inilah yang diterbitkan atau disajikan ke browser.

Sintaks yang butuh transformasi, bukan sekadar penghapusan#

Sebagian sintaks TypeScript tidak bisa hilang begitu saja karena bentuknya tidak ada di JavaScript. Contoh yang paling sering ditemui adalah enum, yang harus diterjemahkan menjadi objek JavaScript agar nilainya dapat dipakai saat program berjalan. Berkas peringkat.ts mendefinisikan satu enum dan dijalankan dengan Node.js melalui mode transformasinya.

ts peringkat.ts
// tidak-dijalankan
enum Peringkat {
  Rendah = 1,
  Sedang = 2,
  Tinggi = 3,
}

const p: Peringkat = Peringkat.Sedang;
console.log(p, Peringkat[p]);
bash
# menjalankan enum melalui mode transformasi Node.js
node --experimental-transform-types --no-warnings peringkat.ts
text Keluaran
2 Sedang
bash
# memperlihatkan hasil transformasi enum oleh esbuild
esbuild --loader=ts < peringkat.ts
text Keluaran
var Peringkat = /* @__PURE__ */ ((Peringkat2) => {
  Peringkat2[Peringkat2["Rendah"] = 1] = "Rendah";
  Peringkat2[Peringkat2["Sedang"] = 2] = "Sedang";
  Peringkat2[Peringkat2["Tinggi"] = 3] = "Tinggi";
  return Peringkat2;
})(Peringkat || {});
const p = 2 /* Sedang */;
console.log(p, Peringkat[p]);

Hasil transformasinya berupa objek dengan pemetaan dua arah antara nama dan angka, lengkap dengan penanda @__PURE__ yang membantu proses tree-shaking, yaitu pembuangan kode yang tidak terpakai, saat bundling. Node.js dalam mode bawaannya menolak sintaks semacam ini karena hanya membuang tipe tanpa menerjemahkan apa pun, sebagaimana diperlihatkan perintah berikut.

bash
# menjalankan berkas enum dengan mode penghapusan tipe bawaan Node.js
node peringkat.ts 2>&1 | grep -m1 "strip-only mode"
text Keluaran
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode

Eksekusi yang berhasil pada contoh pertama terjadi karena flag --experimental-transform-types dinyalakan, yaitu mode transformasi Node.js yang sejajar dengan esbuild. Alat tsx mengambil pendekatan yang sama dengan menjalankan berkas TypeScript setelah mengubahnya dengan esbuild.

bash
# tidak-dijalankan
tsx laporan-kata.ts

Perintah tsx tidak tersedia pada lingkungan kerja topik ini, sehingga contohnya diberi tanda tanpa eksekusi dan konsepnya diperlihatkan melalui esbuild beserta flag transformasi Node.js. Pada mesin yang memasang tsx, perintah tersebut menghasilkan keluaran 6 kata yang sama dengan contoh sebelumnya.

Batas esbuild dan posisi tsc#

esbuild menangani sintaks dengan baik, tetapi tipenya tidak pernah diperiksa. Berkas laporan-salah.ts di bawah ini meneruskan 120 ke parameter yang tipenya string, dan kesalahan tersebut sengaja dipertahankan untuk memperlihatkan sikap kedua alat.

ts laporan-salah.ts
// tidak-dijalankan
import { hitungKata, type OpsiHitung } from "./hitung-kata.ts";

const opsi: OpsiHitung = { abaikanKapital: true };
console.log(hitungKata(120, opsi));
bash
# membundle berkas yang mengandung type error lalu menjalankan hasilnya
rm -rf /tmp/esb-salah && mkdir -p /tmp/esb-salah
cp hitung-kata.ts laporan-salah.ts /tmp/esb-salah/
( cd /tmp/esb-salah && esbuild laporan-salah.ts --bundle --outfile=bundle.js --log-level=silent && node bundle.js 2>&1 | grep -m1 "TypeError" )
text Keluaran
TypeError: teks.toLowerCase is not a function
bash
# memeriksa berkas yang sama dengan tsc
tsc --noEmit --pretty false --allowImportingTsExtensions laporan-salah.ts
text Keluaran
laporan-salah.ts(5,24): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.

esbuild menyelesaikan bundle tanpa satu pun keluhan, dan programnya baru gagal saat dijalankan karena angka 120 tidak memiliki metode toLowerCase. Perintah tsc pada blok berikutnya menunjuk baris dan penyebabnya secara persis, dengan flag --allowImportingTsExtensions yang menyelaraskan pemeriksaan terhadap specifier berakhiran .ts. Urutan kejadian ini menjelaskan pembagian peran keduanya secara lengkap.

AlatTugas utamaMemeriksa tipe
esbuildtransformasi dan bundlingtidak
Viteserver pengembangan dan buildtidak
tsxeksekusi berkas TypeScripttidak
tscpemeriksaan tipe dan kompilasiya

Vite dan pembagian peran dalam proyek#

Vite adalah alat build untuk aplikasi web yang menggabungkan server pengembangan dengan proses build produksi. Pada saat pengembangan, Vite menyajikan modul sesuai permintaan browser dan memakai esbuild untuk transformasinya, sedangkan build produksinya memakai bundler berbasis Rollup. tsc tidak berada di jalur panas (hot path), yaitu bagian yang dieksekusi berulang kali, karena pemeriksaan tipe seluruh proyek terlalu lambat untuk dilakukan pada setiap perubahan berkas. Konfigurasi Vite biasanya berada pada berkas vite.config.ts seperti contoh berikut, dan contoh itu tidak dieksekusi di lingkungan kerja ini karena Vite tidak dipasang di sana.

ts Contoh: vite.config.ts
// tidak-dijalankan
import { defineConfig } from "vite";

export default defineConfig({
  build: {
    outDir: "dist",
  },
});

Pola yang lazim adalah memisahkan gerbang pemeriksaan tipe dari perintah build melalui skrip pada package.json.

json Contoh: skrip typecheck terpisah
{
  "scripts": {
    "typecheck": "tsc --noEmit",
    "build": "vite build"
  }
}

Skrip typecheck memeriksa tipe tanpa menghasilkan berkas apa pun, sedangkan build mengerjakan bundling dengan kecepatan penuh. Keduanya dapat dijalankan berurutan pada CI, sehingga kesalahan tipe tertangkap sebelum hasil build diterbitkan. Dengan susunan ini, kecepatan esbuild dan ketelitian tsc sama-sama didapat tanpa saling menunggu.

Kesalahan umum#

Kesalahan berikut sering muncul saat menata perkakas build untuk proyek TypeScript.

  • Menganggap build yang berhasil sebagai bukti bahwa tipe aman. esbuild dan Vite tidak memeriksa tipe, sehingga tsc --noEmit tetap harus dijalankan sebagai gerbang tersendiri.
  • Menuntut tsc memeriksa setiap perubahan berkas pada dev server. Pemeriksaan seluruh proyek terlalu lambat untuk diulang pada setiap perubahan berkas, dan itulah alasan Vite meletakkan tsc di luar jalur panas.
  • Mengira alat eksekusi seperti node dan tsx melakukan pemeriksaan tipe. Keduanya hanya mengeksekusi hasil transformasi, sehingga kesalahan tipe tetap lolos sampai tsc dipanggil.
  • Mencari pesan error tipe pada keluaran esbuild. Pesan esbuild berupa pesan sintaks atau resolusi berkas, sedangkan pesan tipe seperti TS2345 hanya berasal dari tsc.

Ringkasan#

  • esbuild mengubah dan menggabungkan berkas TypeScript dengan sangat cepat, tetapi tipenya tidak pernah diperiksa dan yang dibuang hanyalah anotasinya.
  • Sintaks seperti enum menuntut transformasi, sehingga mode penghapusan tipe bawaan Node.js menolaknya, sedangkan esbuild dan flag --experimental-transform-types menerjemahkannya.
  • tsc berdiri terpisah sebagai pemeriksa tipe, dan pola yang lazim adalah skrip tsc --noEmit di samping perintah build yang cepat.
  • Vite menyediakan server pengembangan dan build produksi dengan esbuild di dalamnya, sedangkan tsx mengeksekusi berkas TypeScript melalui transformasi yang sama.

Referensi#