Menyelesaikan Konflik Asset
Frontend panel dipublikasikan ke dalam aplikasi Anda, sehingga setiap file tersebut menjadi milik aplikasi — artinya pembaruan package tidak dapat memperbaruinya secara diam-diam. php artisan panel:assets adalah cara proses upgrade mengetahui file hasil publish mana yang tertinggal, file mana yang Anda ubah, dan file mana yang berubah di kedua sisi; halaman ini membahas kasus ketiga dan cara menanganinya. Gunakan panduan ini setelah composer update chocoalano/panel melaporkan CONFLICT, atau ketika sebuah upgrade tidak membawa komponen yang Anda harapkan.
Contoh minimal yang dapat langsung digunakan
composer update chocoalano/panel
php artisan panel:assets # what is behind, what is yours, what conflicts
php artisan panel:assets --update # write only the files you have never edited
npm run build2
3
4
5
Pada aplikasi yang belum pernah mengedit file hasil publish, langkah di atas sudah cukup. Bagian berikutnya menjelaskan apa yang harus dilakukan ketika --update masih menyisakan file yang tidak diperbarui.
Mengapa konflik bisa terjadi
Komponen panel disalin ke resources/js, bukan diimpor langsung dari vendor/chocoalano/panel. Ini disengaja: setiap component registry merupakan allowlist import.meta.glob pada saat build terhadap tree milik aplikasi sendiri, sehingga komponen yang tidak pernah terlihat oleh proses build tidak dapat di-resolve, dan komponen yang source-nya tidak dapat Anda baca juga tidak dapat Anda debug. Konsekuensinya adalah seluruh masalah yang dibahas di halaman ini — setelah sebuah file menjadi milik aplikasi, package update tidak dapat memperbaruinya begitu saja.
vendor:publish tidak dapat menyelesaikan masalah ini karena hanya memiliki dua perilaku, dan keduanya tidak tepat untuk proses upgrade:
| Hasil | |
|---|---|
php artisan vendor:publish --tag=panda-panel-assets | Melewati semua file yang sudah ada. Tidak ada yang diperbarui. |
php artisan vendor:publish --tag=panda-panel-assets --force | Menimpa semuanya, termasuk perubahan yang memang Anda buat dengan sengaja. |
Keduanya tidak dapat membedakan file yang hanya tertinggal dari versi package dengan file yang sengaja diedit, karena dalam kedua kasus tersebut file sama-sama "berbeda dari salinan milik package".
Perbandingan tiga arah
Nilai yang hilang adalah nilai yang dalam Git disediakan oleh git merge-base: catatan tentang common ancestor. PandaPanel\Support\Installer\AssetManifest menyimpannya di .panel-assets.json pada root aplikasi — yaitu hash yang dimiliki setiap file hasil publish pada saat file tersebut dipublikasikan. Dengan nilai ketiga ini, tiga pertanyaan dapat dijawab secara pasti.
| Pertanyaan | Yang dibandingkan | Jawaban |
|---|---|---|
| Apakah Anda mengubahnya? | file di disk vs. manifest | edited |
| Apakah package mengubahnya? | file di package vs. manifest | updated |
| Perubahan terjadi di sisi mana? | kedua perbandingan di atas | status file |
// PandaPanel\Support\Installer\AssetManifest::statusFor(), in essence
$edited = $onDisk !== $recorded;
$updated = $inPackage !== $recorded;
match (true) {
$edited && $updated => AssetManifest::CONFLICT,
$edited => AssetManifest::MODIFIED,
$updated => AssetManifest::STALE,
default => AssetManifest::CURRENT,
};2
3
4
5
6
7
8
9
10
11
Dengan begitu tersedia empat jawaban utama, dibandingkan hanya dua jawaban yang dimiliki vendor:publish:
| Di disk | Di package | Status | Artinya | --update | --force |
|---|---|---|---|---|---|
| = manifest | = manifest | current | tidak berubah di kedua sisi | tidak | tidak |
| = manifest | ≠ manifest | stale | tertinggal — belum pernah diedit di aplikasi | menulis | menulis |
| ≠ manifest | = manifest | modified | milik Anda — tidak ada perubahan baru dari upstream | tidak | menulis |
| ≠ manifest | ≠ manifest | conflict | keduanya berubah | tidak | menulis |
| tidak ada | ada | deleted | Anda menghapusnya | tidak | tidak |
| tidak ada di manifest, berbeda | ada | new | file baru sejak terakhir publish | menulis | menulis |
| tidak ada di manifest, identik | ada | current | sudah dipublish sebelum manifest tersedia | tidak | tidak |
| ada di manifest | tidak lagi dikirim | removed-upstream | tidak lagi disediakan upstream | tidak | tidak |
--update hanya menulis dua kategori tersebut, dan pada keduanya aplikasi terbukti belum memiliki keputusan atau modifikasi sendiri terhadap file: satu file belum pernah dimiliki, dan satu lagi belum pernah disentuh. Konflik hanya dilaporkan beserta path-nya dan tidak pernah diselesaikan dengan tebakan otomatis — konflik adalah diff yang memang perlu dibaca manusia.
Ketujuh status tersedia sebagai public constant, sehingga kode dapat membandingkan constant daripada menulis string status secara manual:
use PandaPanel\Support\Installer\AssetManifest;
AssetManifest::NEW; // 'new'
AssetManifest::CURRENT; // 'current'
AssetManifest::STALE; // 'stale'
AssetManifest::MODIFIED; // 'modified'
AssetManifest::CONFLICT; // 'conflict'
AssetManifest::DELETED; // 'deleted'
AssetManifest::REMOVED_UPSTREAM; // 'removed-upstream'2
3
4
5
6
7
8
9
Struktur file manifest dan API lengkapnya dibahas di Manifest asset.
Apa yang dilaporkan panel:assets
php artisan panel:assets new ................................................................ 3
out of date ....................................................... 12
CONFLICT ........................................................... 1
yours .............................................................. 4
current .......................................................... 284
WARN 1 file(s) changed both here and upstream. Neither copy is safe to throw away, so
nothing was written. Diff each against the package copy under vendor/chocoalano/panel,
then re-run with --force once you have merged:
resources/js/panel/tables/DataTable.vue
INFO Run `php artisan panel:assets --update` to write the safe ones.2
3
4
5
6
7
8
9
10
11
12
13
Ada tiga hal penting tentang output tersebut.
Label yang ditampilkan tidak sama dengan nama constant. Setiap status ditampilkan dengan label yang dibuat agar mudah dibaca manusia:
| Status | Ditampilkan sebagai | Warna |
|---|---|---|
new | new | hijau |
stale | out of date | kuning |
conflict | CONFLICT | merah |
modified | yours | biru |
deleted | deleted by you | abu-abu |
removed-upstream | no longer shipped | abu-abu |
current | current | abu-abu |
Status dengan jumlah nol tidak ditampilkan.
Hanya konflik yang ditampilkan lengkap beserta path-nya. File current biasanya berjumlah sangat banyak; menuliskan ratusan path hanya akan membuat laporan sulit dibaca. Jika Anda ingin melihat path dari status lain, baca report melalui PHP — lihat bagian di bawah.
Exit code selalu 0. Konflik bukan berarti command gagal. Command berhasil menjalankan tugasnya dan menemukan sesuatu yang memang membutuhkan keputusan manusia. Exit code non-zero justru dapat menggagalkan deployment hanya karena ada file yang sengaja diedit.
Command
PandaPanel\Console\Commands\PanelAssetsCommand:
protected $signature = 'panel:assets
{--update : Write the files that are safe to write}
{--force : Also overwrite files this application has edited}';2
3
| Pemanggilan | Yang ditulis | Menulis ulang .panel-assets.json |
|---|---|---|
php artisan panel:assets | tidak ada | tidak pernah |
php artisan panel:assets --update | new, stale | jika setidaknya satu file ditulis |
php artisan panel:assets --force | kategori di atas, ditambah modified dan conflict | sama |
--force sudah berarti melakukan penulisan — tidak perlu digabung dengan --update — dan memperluas kategori file yang boleh ditulis hanya ke file yang Anda edit. File yang sengaja Anda hapus tetap terhapus, dan file yang tidak lagi dikirim package tidak akan dihidupkan kembali.
Tidak ada flag per-file. Granularitasnya adalah satu eksekusi penuh, sehingga penting memahami pilihan di bawah sebelum menggunakan --force.
Mengambil update pada file yang sudah diedit
Konflik berarti kedua salinan sama-sama memiliki perubahan. Ada tiga hasil yang masuk akal, dan pilihan kedua memerlukan urutan langkah yang benar.
| Yang Anda inginkan | Lakukan ini | Status file setelahnya |
|---|---|---|
| Gunakan versi package, buang perubahan Anda | panel:assets --force | current |
| Pertahankan keduanya — update dan perubahan Anda | Ambil salinan package terlebih dahulu, catat sebagai baseline, lalu terapkan ulang perubahan Anda | modified |
| Pertahankan versi Anda, abaikan update | tidak melakukan apa pun | conflict, pada setiap report sampai salah satu sisi berubah |
Opsi 1 — gunakan salinan dari package
Perubahan Anda sudah tidak perlu dipertahankan, biasanya karena package telah memperbaiki hal yang sama. Lihat diff untuk memastikan, lalu timpa file:
diff -u resources/js/panel/tables/DataTable.vue \
vendor/chocoalano/panel/resources/js/panel/tables/DataTable.vue
php artisan panel:assets --force
php artisan panel:icons # rebuild after panel:assets --force rewrote assets
npm run build2
3
4
5
6
--force menulis semua file berstatus modified dan conflict, bukan hanya file yang sedang Anda periksa. Lihat terlebih dahulu apa saja yang termasuk dalam kategori tersebut:
php artisan panel:assets # read the `yours` count firstOpsi 2 — ambil update dan pertahankan perubahan Anda
Ambil salinan package terlebih dahulu, biarkan manifest mencatat baseline tersebut, lalu terapkan kembali perubahan Anda di atasnya. Jangan dibalik. Alasannya bersifat mekanis: manifest mencatat kondisi file di disk saat manifest ditulis. Jika file hasil merge manual justru direkam sebagai baseline-nya sendiri, file tersebut akan terbaca sebagai out of date pada perbandingan berikutnya — karena salinan package tetap berbeda — lalu --update berikutnya dapat menimpanya tanpa peringatan.
# 1. Baseline: the package's copy becomes the file on disk.
cp vendor/chocoalano/panel/resources/js/panel/tables/DataTable.vue \
resources/js/panel/tables/DataTable.vue
# 2. Record that baseline. panel:install writes the manifest unconditionally,
# publishes nothing over a file that already exists, and scaffolds nothing.
php artisan panel:install --no-panel --no-user --no-interaction
# 3. Re-apply your change to the new file, by hand or from the diff you kept.
git diff HEAD~1 -- resources/js/panel/tables/DataTable.vue
# 4. Confirm.
php artisan panel:assets # the file now reads `yours`
npm run build2
3
4
5
6
7
8
9
10
11
12
13
14
Setelah langkah 3, hash yang tersimpan adalah hash salinan package, sementara file di disk sudah berisi perubahan Anda. Kondisi tersebut tepat sama dengan modified: ada perubahan milik aplikasi dan belum ada perubahan baru dari upstream. --update tidak akan menyentuh file itu. Ketika package kembali mengubah file tersebut pada rilis berikutnya, status berubah menjadi conflict lagi — dan itu adalah jawaban yang benar karena ada perubahan Anda yang perlu digabung dengan perubahan upstream.
Simpan diff sebelum memulai agar langkah 3 benar-benar menjadi proses menerapkan ulang perubahan, bukan mencoba mengingat perubahan sebelumnya:
git diff -- resources/js/panel/tables/DataTable.vue > /tmp/datatable.patchOpsi 3 — pertahankan versi Anda dan abaikan update
Tidak perlu melakukan apa pun. File tetap persis seperti sekarang dan akan dilaporkan sebagai CONFLICT pada setiap eksekusi sampai Anda atau package mengubah file tersebut lagi. Tidak ada opsi "ignore" per file maupun suppression list. Konflik yang terus muncul adalah pernyataan bahwa aplikasi sedang membawa komponen yang sudah tertinggal dari package — informasi tersebut memang benar dan layak terus terlihat.
Konsekuensinya adalah report tidak pernah benar-benar bersih. Pastikan modifikasi tersebut masih layak dipertahankan — lihat Cara menghindari konflik pada upgrade berikutnya.
Mencatat penyelesaian secara manual
AssetManifest::write() selalu menghitung hash dari salinan milik aplikasi, bukan dari package, sehingga tidak ada API resmi yang berarti "anggap file saya sebagai baseline". Jika baseline pada opsi 2 tidak praktis — misalnya Anda menulis ulang file secara besar-besaran — jalan keluarnya adalah menulis hash ke .panel-assets.json secara manual. Hash menggunakan xxh128 atas isi file dengan \r\n dinormalisasi menjadi \n:
use Illuminate\Support\Facades\File;
use PandaPanel\Support\Installer\AssetManifest;
$relative = 'resources/js/panel/tables/DataTable.vue';
$source = base_path('vendor/chocoalano/panel/'.$relative);
$files = AssetManifest::read();
$files[$relative] = hash('xxh128', str_replace("\r\n", "\n", (string) File::get($source)));
ksort($files);
File::put(AssetManifest::path(), json_encode(['files' => $files], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)."\n");2
3
4
5
6
7
8
9
10
11
12
Mencatat hash package untuk file yang sudah Anda edit membuat file tersebut terbaca sebagai modified, bukan conflict. Artinya Anda menyatakan bahwa perubahan upstream sudah diperiksa dan dipertimbangkan. Lakukan hanya jika memang benar, karena eksekusi berikutnya tidak dapat membedakan klaim tersebut dari baseline yang dibuat secara normal.
Membaca report melalui PHP
Semua output command berasal dari satu pemanggilan, sehingga status apa pun dapat didaftar berdasarkan path, di-assert dalam test, atau dijadikan penyebab build gagal.
use PandaPanel\Support\Installer\AssetManifest;
$report = AssetManifest::compare();
$conflicts = array_keys(array_filter(
$report,
static fn (array $entry): bool => $entry['status'] === AssetManifest::CONFLICT,
));
// ['resources/js/panel/tables/DataTable.vue']2
3
4
5
6
7
8
9
10
Setiap entry membawa dua path yang dibutuhkan untuk membuat diff:
$report['resources/js/panel/tables/DataTable.vue'];
// [
// 'status' => 'conflict',
// 'destination' => '/var/www/app/resources/js/panel/tables/DataTable.vue',
// 'source' => '/var/www/app/vendor/chocoalano/panel/resources/js/panel/tables/DataTable.vue',
// ]2
3
4
5
6
Dengan informasi itu Anda juga dapat melakukan diff semua conflict dalam satu proses:
use PandaPanel\Support\Installer\AssetManifest;
foreach (AssetManifest::compare() as $relative => $entry) {
if ($entry['status'] !== AssetManifest::CONFLICT) {
continue;
}
passthru(sprintf('diff -u %s %s', escapeshellarg($entry['destination']), escapeshellarg($entry['source'])));
}2
3
4
5
6
7
8
9
Jika Anda ingin build gagal ketika conflict ditemukan — sesuatu yang sengaja tidak dilakukan command bawaan:
exit($conflicts === [] ? 0 : 1);Menghindari konflik pada upgrade berikutnya
Setiap conflict bermula dari perubahan pada file hasil publish, tetapi tidak semua kebutuhan kustomisasi harus dilakukan dengan mengedit file tersebut. Empat extension point berikut mencakup sebagian besar kasus dan tidak satu pun mengharuskan Anda mengubah file yang juga dikelola package:
| Daripada mengedit | Gunakan | Dokumentasi |
|---|---|---|
| Class pada elemen shell | Panel::cssHooks(array $classes) | CSS hooks |
| Markup yang disisipkan ke shell | Panel::renderHook(RenderHook $hook, string $component, array $data = [], array $scopes = []) | Render hooks |
| Komponen column, field, atau entry | Komponen custom yang Anda registrasikan sendiri | Custom columns, custom fields |
| Satu layar penuh | Custom page atau widget component | Custom pages, custom widgets |
use PandaPanel\Core\Panel;
$panel->cssHooks([
'sidebar' => 'bg-slate-950',
'topbar' => 'border-b-2',
]);2
3
4
5
6
File yang tidak pernah Anda edit adalah file yang selalu dapat diperbarui dengan aman oleh --update; itulah inti dari mekanisme ini.
Hal yang perlu diperhatikan
- Melakukan merge ke file Anda terlebih dahulu adalah jebakannya. Jika file hasil merge direkam sebagai baseline-nya sendiri, statusnya akan terbaca
out of datekarena salinan package tetap berbeda.--updateberikutnya dapat menimpanya tanpa peringatan. Ambil salinan package terlebih dahulu, catat baseline, lalu terapkan ulang perubahan Anda. Itulah opsi 2, dan urutannya penting. --forceberlaku untuk seluruh eksekusi. Opsi ini menulis semua filemodifieddanconflict. Selesaikan file penting secara individual atau setidaknya periksa jumlahyourssebelum menjalankannya.- Jalankan
panel:iconssetelah setiap--force.resources/js/panel/icons/registry.tsadalah file hasil publish yang dihasilkanphp artisan panel:iconsdari icon yang dideklarasikan panel Anda, sehingga--forceakan menggantinya dengan salinan bawaan package. npm run buildadalah bagian dari proses penyelesaian, bukan optimasi tambahan. Komponen hasil publish adalah source; registry menggunakanimport.meta.globyang dievaluasi pada saat build. File yang sudah ditulis ke disk belum masuk bundle sampai build dijalankan.- Manifest hanya ditulis ulang ketika setidaknya satu file berhasil ditulis. Eksekusi yang tidak mengubah apa pun membiarkan manifest tetap seperti sebelumnya. Karena itu opsi 2 memakai
panel:install, yang menulis manifest tanpa syarat. - Menjalankan
panel:assetstanpa opsi tidak pernah menulis apa pun, termasuk manifest. Merekam hash hanya karena pengguna meminta report akan membuat hasil eksekusi berikutnya bergantung pada apakah report pernah diminta. - Perbedaan line ending bukan sebuah edit. Hash menormalisasi
\r\nmenjadi\n, sehingga checkout CRLF tidak membuat semua file terlihat conflict. - Conflict tidak pernah membuat command gagal. Exit code tetap
0. Jika Anda ingin conflict menggagalkan build, gunakan pemeriksaan sendiri seperti contoh di atas. - Asset plugin dikelola dengan command berbeda.
php artisan panel:publishmenyalinnya dan melewati file yang sudah ada kecuali menggunakan--forcemiliknya sendiri. Command tersebut tidak memiliki manifest maupun perbandingan tiga arah. vendor:publish --tag=panda-panel-assets --forcetetap bukan tool yang tepat setelah instalasi pertama. Command itu hanya memiliki perbandingan dua arah yang justru digantikan oleh mekanisme pada halaman ini.
Lihat juga
- Manifest asset — file, status, dan API lengkap
AssetManifest - Panduan upgrade — posisi langkah ini dalam proses upgrade
- Breaking changes, Kebijakan versioning
panel:assets,panel:install,panel:icons, publish tags- Memperbarui asset hasil publish, struktur asset hasil publish
- CSS hooks, Render hooks
- Frontend assets, Component registries
- Frontend build, Icon registry
- Troubleshooting: konflik asset