Halaman ini memberikan contoh kode yang ditulis dalam React dan JavaScript untuk fungsi umum yang mungkin ingin Anda gunakan dalam ekstensi.
Menggunakan Looker Extension SDK
Ekstensi harus membuat koneksi dengan host Looker. Di React, hal ini dilakukan dengan menggabungkan ekstensi dalam komponen ExtensionProvider40
. Komponen ini membuat koneksi dengan host Looker dan menyediakan Looker Extension SDK dan Looker SDK untuk ekstensi.
import React from 'react'
import { ExtensionProvider40 } from '@looker/extension-sdk-react'
import { DemoCoreSDK } from './DemoCoreSDK'
export const App = () => {
return (
<ExtensionProvider40 chattyTimeout={-1}>
<DemoCoreSDK />
</ExtensionProvider40>
)
}
Latar belakang tentang penyedia ekstensi
Penyedia ekstensi mengekspos SDK ekstensi Looker dan API SDK ke ekstensi. Versi penyedia ekstensi yang berbeda telah dibuat sejak framework ekstensi dibuat. Bagian ini menjelaskan histori penyedia ekstensi dan alasan ExtensionProvider40 adalah penyedia yang direkomendasikan.
Penyedia ekstensi pertama adalah ExtensionProvider
, yang mengekspos kedua Looker SDK, versi 3.1 dan 4.0. Kelemahannya adalah menyertakan kedua SDK tersebut akan meningkatkan ukuran paket produksi akhir.
ExtensionProvider2
kemudian dibuat. Hal ini dibuat karena tidak masuk akal bagi ekstensi untuk menggunakan kedua SDK dan memaksa developer untuk memilih salah satunya. Sayangnya, hal ini masih menyebabkan kedua SDK disertakan dalam ukuran paket produksi akhir.
Saat SDK 4.0 dipindahkan ke GA, ExtensionProvider40
dibuat. Keuntungan ExtensionProvider40
adalah developer tidak perlu memilih SDK yang akan digunakan, karena SDK 4.0 adalah satu-satunya versi yang tersedia. Karena SDK 3.1 tidak disertakan dalam paket akhir, hal ini memiliki keunggulan dalam mengurangi ukuran paket.
Untuk menambahkan fungsi dari Looker Extension SDK, pertama-tama Anda harus mendapatkan referensi ke SDK, yang dapat dilakukan dari penyedia atau secara global. Kemudian, Anda dapat memanggil fungsi SDK seperti yang Anda lakukan di aplikasi JavaScript.
- Untuk mengakses SDK dari penyedia, ikuti langkah-langkah berikut:
import { ExtensionContext40 } from '@looker/extension-sdk-react'
export const Comp1 = () => {
const extensionContext = useContext(
ExtensionContext40
)
const { extensionSDK, coreSDK } = extensionContext
- Untuk mengakses SDK secara global (ekstensi harus diinisialisasi sebelum dipanggil), ikuti langkah-langkah berikut:
const coreSDK = getCoreSDK()
Sekarang Anda dapat menggunakan SDK seperti yang Anda lakukan di aplikasi JavaScript apa pun:
const GetLooks = async () => {
try {
const looks = await sdk.ok(sdk.all_looks('id'))
// process looks
. . .
} catch (error) {
// do error handling
. . .
}
}
Membuka bagian lain di instance Looker
Karena ekstensi berjalan di iframe dengan sandbox, Anda tidak dapat membuka tempat lain dalam instance Looker dengan memperbarui objek window.location
induk. Anda dapat menavigasi menggunakan Looker Extension SDK.
Fungsi ini memerlukan hak navigation
.
import { ExtensionContext40 } from '@looker/extension-sdk-react'
. . .
const extensionContext = useContext(
ExtensionContext40
)
const { extensionSDK } = extensionContext
. . .
extensionSDK.updateLocation('/browse')
Membuka jendela browser baru
Karena ekstensi berjalan di iframe dengan sandbox, Anda tidak dapat menggunakan jendela induk untuk membuka jendela browser baru. Anda dapat membuka jendela browser menggunakan Looker Extension SDK.
Fungsi ini memerlukan hak new_window
untuk membuka jendela baru ke lokasi di instance Looker saat ini, atau hak new_window_external_urls
untuk membuka jendela baru yang berjalan di host lain.
import { ExtensionContext40 } from '@looker/extension-sdk-react'
. . .
const extensionContext = useContext(
ExtensionContext40
)
const { extensionSDK } = extensionContext
. . .
extensionSDK.openBrowserWindow('/browse', '_blank')
. . .
extensionSDK.openBrowserWindow('https://docs.looker.com/reference/manifest-params/application#entitlements', '_blank')
Pemilihan rute dan deep linking
Hal berikut berlaku untuk ekstensi berbasis React.
Komponen ExtensionProvider
, ExtensionProvider2
, dan ExtensionProvider40
akan otomatis membuat React Router bernama MemoryRouter
untuk Anda gunakan. Jangan mencoba membuat BrowserRouter
, karena tidak berfungsi di iframe dengan sandbox. Jangan mencoba membuat HashRouter
, karena tidak berfungsi di iframe dengan sandbox untuk browser Microsoft Edge versi non-Chromium.
Jika MemoryRouter
digunakan dan Anda menggunakan react-router
di ekstensi, framework ekstensi akan otomatis menyinkronkan router ekstensi ke router host Looker. Artinya, ekstensi akan diberi tahu tentang klik tombol mundur dan maju browser serta rute saat ini saat halaman dimuat ulang. Hal ini juga berarti bahwa ekstensi akan otomatis mendukung deep linking. Lihat contoh ekstensi untuk mengetahui cara menggunakan react-router
.
Data konteks ekstensi
Data konteks framework ekstensi tidak boleh disamakan dengan konteks React.
Ekstensi memiliki kemampuan untuk membagikan data konteks di antara semua pengguna ekstensi. Data konteks dapat digunakan untuk data yang tidak sering berubah dan tidak memiliki persyaratan keamanan khusus. Anda harus berhati-hati saat menulis data, karena tidak ada penguncian data dan operasi tulis terakhir yang menang. Data konteks segera tersedia untuk ekstensi setelah dimulai. Looker Extension SDK menyediakan fungsi untuk memungkinkan data konteks diperbarui dan dimuat ulang.
Ukuran maksimum data konteks adalah sekitar 16 MB. Data konteks akan diserialisasi ke string JSON, sehingga hal ini juga perlu dipertimbangkan jika Anda menggunakan data konteks untuk ekstensi.
import { ExtensionContext40 } from '@looker/extension-sdk-react'
. . .
const extensionContext = useContext(
ExtensionContext40
)
const { extensionSDK } = extensionContext
. . .
// Get loaded context data. This will reflect any updates that have
// been made by saveContextData.
let context = await extensionSDK.getContextData()
. . .
// Save context data to Looker server.
context = await extensionSDK.saveContextData(context)
. . .
// Refresh context data from Looker server.
context = await extensionSDK.refreshContextData()
Atribut pengguna
Looker Extension SDK menyediakan API untuk mengakses atribut pengguna Looker. Ada dua jenis akses atribut pengguna:
Tercakup — Terkait dengan ekstensi. Atribut pengguna yang dicakup diberi namespace ke ekstensi dan atribut pengguna harus ditentukan di instance Looker sebelum dapat digunakan. Untuk membuat namespace atribut pengguna, beri awalan nama atribut dengan nama ekstensi. Tanda hubung dan karakter '::' dalam nama ekstensi harus diganti dengan garis bawah, karena tanda hubung dan titik dua tidak dapat digunakan dalam nama atribut pengguna.
Misalnya: atribut pengguna cakupan bernama
my_value
yang digunakan dengan ID ekstensimy-extension::my-extension
harus memiliki nama atribut penggunamy_extension_my_extension_my_value
yang ditentukan. Setelah ditentukan, atribut pengguna dapat dibaca dan diperbarui oleh ekstensi.Global — Ini adalah atribut pengguna global dan bersifat hanya baca. Contohnya adalah atribut pengguna
locale
.
Berikut adalah daftar panggilan API atribut pengguna:
userAttributeGetItem
— Membaca atribut pengguna. Nilai default dapat ditentukan dan akan digunakan jika nilai atribut pengguna tidak ada untuk pengguna tersebut.userAttributeSetItem
— Menyimpan atribut pengguna untuk pengguna saat ini. Akan gagal untuk atribut pengguna global. Nilai tersimpan hanya dapat dilihat oleh pengguna saat ini.userAttributeResetItem
— Mereset atribut pengguna untuk pengguna saat ini ke nilai default. Akan gagal untuk atribut pengguna global.
Untuk mengakses atribut pengguna, Anda harus menentukan nama atribut di hak global_user_attributes
dan/atau scoped_user_attributes
. Misalnya, di file manifes project LookML, Anda akan menambahkan:
entitlements: {
scoped_user_attributes: ["my_value"]
global_user_attributes: ["locale"]
}
import { ExtensionContext40 } from '@looker/extension-sdk-react'
. . .
const extensionContext = useContext(
ExtensionContext40
)
const { extensionSDK } = extensionContext
// Read global user attribute
const locale = await extensionSDK.userAttributeGetItem('locale')
// Read scoped user attribute
const value = await extensionSDK.userAttributeGetItem('my_value')
// Update scoped user attribute
const value = await extensionSDK.userAttributeSetItem('my_value', 'abcd1234')
// Reset scoped user attribute
const value = await extensionSDK.userAttributeResetItem('my_value')
Penyimpanan lokal
Iframe dalam sandbox tidak mengizinkan akses ke penyimpanan lokal browser. Looker Extension SDK memungkinkan ekstensi membaca dan menulis ke penyimpanan lokal jendela induk. Penyimpanan lokal diberi namespace ke ekstensi, yang berarti tidak dapat membaca penyimpanan lokal yang dibuat oleh jendela induk atau ekstensi lainnya.
Penggunaan penyimpanan lokal memerlukan hak local_storage
.
API localhost ekstensi bersifat asinkron, bukan API penyimpanan lokal browser sinkron.
import { ExtensionContext40 } from '@looker/extension-sdk-react'
. . .
const extensionContext = useContext(
ExtensionContext40
)
const { extensionSDK } = extensionContext
// Read from local storage
const value = await extensionSDK.localStorageGetItem('my_storage')
// Write to local storage
await extensionSDK.localStorageSetItem('my_storage', 'abcedefh')
// Delete item from local storage
await extensionSDK.localStorageRemoveItem('my_storage')
Memperbarui judul halaman
Ekstensi dapat memperbarui judul halaman saat ini. Anda tidak memerlukan hak untuk melakukan tindakan ini.
import { ExtensionContext40 } from '@looker/extension-sdk-react'
. . .
const extensionContext = useContext(
ExtensionContext40
)
const { extensionSDK } = extensionContext
extensionSDK.updateTitle('My Extension Title')
Menulis ke papan klip sistem
Iframe dengan sandbox tidak mengizinkan akses ke papan klip sistem. Looker Extension SDK memungkinkan ekstensi menulis teks ke papan klip sistem. Untuk tujuan keamanan, ekstensi tidak diizinkan untuk membaca dari papan klip sistem.
Untuk menulis ke papan klip sistem, Anda memerlukan hak use_clipboard
.
import { ExtensionContext40 } from '@looker/extension-sdk-react'
. . .
const extensionContext = useContext(
ExtensionContext40
)
const { extensionSDK } = extensionContext
// Write to system clipboard
try {
await extensionSDK.clipboardWrite(
'My interesting information'
)
. . .
} catch (error) {
. . .
}
Menyematkan dasbor, Look, dan Jelajah
Framework ekstensi mendukung penyematan dasbor, Tampilan, dan Jelajah.
Hak use_embeds
wajib diisi. Sebaiknya gunakan Looker JavaScript Embed SDK untuk menyematkan konten. Lihat dokumentasi Embed SDK untuk informasi selengkapnya.
import { ExtensionContext40 } from '@looker/extension-sdk-react'
. . .
const extensionContext = useContext(
ExtensionContext40
)
const { extensionSDK } = extensionContext
. . .
const canceller = (event: any) => {
return { cancel: !event.modal }
}
const updateRunButton = (running: boolean) => {
setRunning(running)
}
const setupDashboard = (dashboard: LookerEmbedDashboard) => {
setDashboard(dashboard)
}
const embedCtrRef = useCallback(
(el) => {
const hostUrl = extensionContext?.extensionSDK?.lookerHostData?.hostUrl
if (el && hostUrl) {
el.innerHTML = ''
LookerEmbedSDK.init(hostUrl)
const db = LookerEmbedSDK.createDashboardWithId(id as number)
.withNext()
.appendTo(el)
.on('dashboard:loaded', updateRunButton.bind(null, false))
.on('dashboard:run:start', updateRunButton.bind(null, true))
.on('dashboard:run:complete', updateRunButton.bind(null, false))
.on('drillmenu:click', canceller)
.on('drillmodal:explore', canceller)
.on('dashboard:tile:explore', canceller)
.on('dashboard:tile:view', canceller)
.build()
.connect()
.then(setupDashboard)
.catch((error: Error) => {
console.error('Connection error', error)
})
}
},
[]
)
return (<EmbedContainer ref={embedCtrRef} />)
Contoh ekstensi menggunakan komponen bergaya untuk memberikan gaya sederhana ke iframe yang dihasilkan. Contoh:
import styled from "styled-components"
export const EmbedContainer = styled.div`
width: 100%;
height: 95vh;
& > iframe {
width: 100%;
height: 100%;
}
Mengakses endpoint API eksternal
Framework ekstensi menyediakan dua metode untuk mengakses endpoint API eksternal:
- Proxy server — Mengakses endpoint melalui server Looker. Mekanisme ini memungkinkan client ID dan kunci rahasia ditetapkan dengan aman oleh server Looker.
- Proxy pengambilan — Mengakses endpoint dari browser pengguna. Proxy adalah UI Looker.
Dalam kedua kasus tersebut, Anda harus menentukan endpoint API eksternal di hak external_api_urls
ekstensi.
Server proxy
Contoh berikut menunjukkan penggunaan proxy server untuk mendapatkan token akses yang akan digunakan oleh proxy pengambilan. Client ID dan secret harus ditentukan sebagai atribut pengguna untuk ekstensi. Biasanya, saat atribut pengguna disiapkan, nilai default ditetapkan ke client id atau secret.
import { ExtensionContext40 } from '@looker/extension-sdk-react'
. . .
const extensionContext = useContext(
ExtensionContext40
)
const { extensionSDK } = extensionContext
. . .
const requestBody = {
client_id: extensionSDK.createSecretKeyTag('my_client_id'),
client_secret: extensionSDK.createSecretKeyTag('my_client_secret'),
},
try {
const response = await extensionSDK.serverProxy(
'https://myaccesstokenserver.com/access_token',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
}
)
const { access_token, expiry_date } = response.body
. . .
} catch (error) {
// Error handling
. . .
}
Nama atribut pengguna harus dipetakan ke ekstensi. Tanda hubung harus diganti dengan garis bawah dan karakter ::
harus diganti dengan satu garis bawah.
Misalnya, jika nama ekstensi Anda adalah my-extension::my-extension
, atribut pengguna yang perlu ditentukan untuk contoh sebelumnya adalah sebagai berikut:
my_extension_my_extension_my_client_id
my_extension_my_extension_'my_client_secret'
Mengambil proxy
Contoh berikut menunjukkan penggunaan proxy pengambilan. Contoh ini menggunakan token akses dari contoh proxy server sebelumnya.
import { ExtensionContext40 } from '@looker/extension-sdk-react'
. . .
const extensionContext = useContext(
ExtensionContext40
)
const { extensionSDK } = extensionContext
. . .
try {
const response = await extensionSDK.fetchProxy(
'https://myaccesstokenserver.com/myendpoint',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
some_value: someValue,
another_value: anotherValue,
}),
}
)
// Handle success
. . .
} catch (error) {
// Handle failure
. . .
}
Integrasi OAuth
Framework ekstensi mendukung integrasi dengan penyedia OAuth. OAuth dapat digunakan untuk mendapatkan token akses guna mengakses resource tertentu, misalnya dokumen Google Spreadsheet.
Anda harus menentukan endpoint server OAuth di hak extension oauth2_urls
. Anda mungkin juga perlu menentukan URL tambahan di hak external_api_urls
.
Framework ekstensi mendukung alur berikut:
- Alur implisit
- Jenis pemberian kode otorisasi dengan kunci rahasia
- Tantangan dan pemverifikasi kode PKCE
Alur umumnya adalah jendela turunan dibuka yang memuat halaman server OAuth. Server OAuth mengautentikasi pengguna dan mengalihkan kembali ke server Looker dengan detail tambahan yang dapat digunakan untuk mendapatkan token akses.
Alur implisit:
import { ExtensionContext40 } from '@looker/extension-sdk-react'
. . .
const extensionContext = useContext(
ExtensionContext40
)
const { extensionSDK } = extensionContext
. . .
const response = await extensionSDK.oauth2Authenticate(
'https://accounts.google.com/o/oauth2/v2/auth',
{
client_id: GOOGLE_CLIENT_ID!,
scope: GOOGLE_SCOPES,
response_type: 'token',
}
)
const { access_token, expires_in } = response
Jenis pemberian kode otorisasi dengan kunci rahasia:
const authenticateParameters: Record<string, string> = {
client_id: GITHUB_CLIENT_ID!,
response_type: 'code',
}
const response = await extensionSDK.oauth2Authenticate(
'https://github.com/login/oauth/authorize',
authenticateParameters,
'GET'
)
const exchangeParameters: Record<string, string> = {
client_id: GITHUB_CLIENT_ID!,
code: response.code,
client_secret: extensionSDK.createSecretKeyTag('github_secret_key'),
}
const codeExchangeResponse = await extensionSDK.oauth2ExchangeCodeForToken(
'https://github.com/login/oauth/access_token',
exchangeParameters
)
const { access_token, error_description } = codeExchangeResponse
Tantangan dan pemverifikasi kode PKCE:
import { ExtensionContext40 } from '@looker/extension-sdk-react'
. . .
const extensionContext = useContext(
ExtensionContext40
)
const { extensionSDK } = extensionContext
. . .
const authRequest: Record<string, string> = {
client_id: AUTH0_CLIENT_ID!,
response_type: 'code',
scope: AUTH0_SCOPES,
code_challenge_method: 'S256',
}
const response = await extensionSDK.oauth2Authenticate(
'https://sampleoauthserver.com/authorize',
authRequest,
'GET'
)
const exchangeRequest: Record<string, string> = {
grant_type: 'authorization_code',
client_id: AUTH0_CLIENT_ID!,
code: response.code,
}
const codeExchangeResponse = await extensionSDK.oauth2ExchangeCodeForToken(
'https://sampleoauthserver.com/login/oauth/token',
exchangeRequest
)
const { access_token, expires_in } = codeExchangeResponse
Spartan
Spartan mengacu pada metode penggunaan instance Looker sebagai lingkungan untuk mengekspos ekstensi, dan hanya ekstensi, kepada sekumpulan pengguna yang ditetapkan. Pengguna sederhana yang membuka instance Looker akan melihat alur login apa pun yang telah dikonfigurasi oleh admin Looker. Setelah pengguna diautentikasi, ekstensi akan ditampilkan kepada pengguna sesuai dengan atribut pengguna landing_page
mereka seperti yang ditunjukkan di samping. Pengguna hanya dapat mengakses ekstensi; mereka tidak dapat mengakses bagian lain Looker. Jika pengguna memiliki akses ke beberapa ekstensi, ekstensi tersebut akan mengontrol kemampuan pengguna untuk membuka ekstensi lain menggunakan extensionSDK.updateLocation
. Ada satu metode Looker Extension SDK tertentu untuk memungkinkan pengguna logout dari instance Looker.
import { ExtensionContext40 } from '@looker/extension-sdk-react'
. . .
const extensionContext = useContext(
ExtensionContext40
)
const { extensionSDK } = extensionContext
. . .
// Navigate to another extension
extensionSDK.updateLocation('/spartan/another::extension')
. . .
// Logout
extensionSDK.spartanLogout()
Mendefinisikan pengguna spartan
Untuk menentukan pengguna spartan, Anda harus membuat grup yang disebut "Khusus Ekstensi".
Setelah grup "Khusus Ekstensi" dibuat, buka halaman Atribut Pengguna di bagian Admin Looker dan edit atribut pengguna landing_page
. Pilih tab Nilai Grup dan tambahkan grup "Khusus Ekstensi". Nilai harus ditetapkan ke /spartan/my_extension::my_extension/
dengan my_extension::my_extension
adalah ID ekstensi Anda. Sekarang, saat pengguna tersebut login, pengguna akan diarahkan ke ekstensi yang ditetapkan.
Pemisahan kode
Pemisahan kode adalah teknik saat kode diminta hanya jika diperlukan. Biasanya, potongan kode dikaitkan dengan rute React, dengan setiap rute mendapatkan potongan kodenya sendiri. Di React, hal ini dilakukan dengan komponen Suspense
dan React.lazy
. Komponen Suspense
menampilkan komponen penggantian saat potongan kode dimuat. React.lazy
bertanggung jawab untuk memuat potongan kode.
Menyiapkan pemisahan kode:
import { AsyncComp1 as Comp1 } from './Comp1.async'
import { AsyncComp1 as Comp2 } from './Comp2.async'
. . .
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route path="/comp1">
<Comp1 />
</Route>
<Route path="/comp2">
<Comp2 />
</Route>
</Switch>
<Suspense>
Komponen yang dimuat lambat diimplementasikan sebagai berikut:
import { lazy } from 'react'
const Comp1 = lazy(
async () => import(/* webpackChunkName: "comp1" */ './Comp1')
)
export const AsyncComp1 = () => <Home />
Komponen diimplementasikan sebagai berikut. Komponen harus diekspor sebagai komponen default:
const Comp1 = () => {
return (
<div>Hello World</div>
)
}
export default Comp1
Tree shaking
Meskipun Looker SDK saat ini mendukung tree-shaking, fungsi ini masih perlu ditingkatkan. Kami terus memodifikasi SDK untuk meningkatkan dukungan tree shaking. Beberapa perubahan ini mungkin mengharuskan Anda memfaktorkan ulang kode untuk memanfaatkannya, tetapi jika diperlukan, perubahan tersebut akan didokumentasikan dalam catatan rilis.
Untuk menggunakan tree-shaking, modul yang Anda gunakan harus diekspor sebagai esmodule dan fungsi yang Anda impor harus bebas dari efek samping. Looker SDK untuk TypeScript/Javascript, Looker SDK Runtime Library, Looker UI Components, Looker Extension SDK, dan Extension SDK untuk React semuanya memenuhi persyaratan ini.
Dalam ekstensi, gunakan Looker SDK 4.0, dan gunakan komponen ExtensionProvider2
atau ExtensionProvider40
dari Extension SDK for React.
Kode berikut menyiapkan penyedia ekstensi. Anda harus memberi tahu penyedia SDK yang Anda inginkan:
import { MyExtension } from './MyExtension'
import { ExtensionProvider40 } from '@looker/extension-sdk-react'
import { Looker40SDK } from '@looker/sdk/lib/4.0/methods'
import { hot } from 'react-hot-loader/root'
export const App = hot(() => {
return (
<ExtensionProvider2 type={Looker40SDK}>
<MyExtension />
</ExtensionProvider2>
)
})
Jangan gunakan gaya impor berikut di ekstensi Anda:
import * as lookerComponents from `@looker/components`
Contoh sebelumnya menghadirkan semua hal dari modul. Sebagai gantinya, hanya impor komponen yang benar-benar Anda perlukan. Contoh:
import { Paragraph } from `@looker/components`
Glosarium
- Pemisahan kode — Teknik untuk pemuatan lambat JavaScript hingga benar-benar diperlukan. Idealnya, Anda ingin membuat paket JavaScript yang dimuat di awal sekecil mungkin. Hal ini dapat dicapai dengan memanfaatkan pemisahan kode. Fungsi apa pun yang tidak segera diperlukan tidak akan dimuat hingga benar-benar diperlukan.
- IDE — Integrated development environment (Lingkungan pengembangan terintegrasi). Editor yang digunakan untuk membuat dan mengubah ekstensi. Contohnya adalah Visual Studio Code, Intellij, dan WebStorm.
- Scene — Umumnya kunjungan halaman di Looker. Scene dipetakan ke rute utama. Terkadang, sebuah scene akan memiliki scene turunan yang dipetakan ke subrute dalam rute utama.
- Transpile — Proses mengambil kode sumber yang ditulis dalam satu bahasa dan mengubahnya menjadi bahasa lain yang memiliki tingkat abstraksi serupa. Contohnya adalah TypeScript ke JavaScript.