Manifest Asset
.panel-assets.json adalah catatan kondisi frontend panel yang telah dipublikasikan pada saat aplikasi mempublikasikannya. File ini menyediakan nilai ketiga yang memungkinkan proses upgrade membedakan file yang Anda edit dari file yang hanya tertinggal dari versi package, dan file inilah yang dibaca oleh php artisan panel:assets. Gunakan halaman ini ketika Anda perlu mengetahui isi file tersebut, command apa yang menulisnya, serta cara menggunakan class yang bekerja di balik mekanisme ini.
Contoh minimal yang dapat langsung digunakan
php artisan panel:assets # compare, report, write nothing
php artisan panel:assets --update # write the files that are safe to write2
use PandaPanel\Support\Installer\AssetManifest;
AssetManifest::path(); // '/var/www/app/.panel-assets.json'
AssetManifest::exists(); // true after an install
AssetManifest::read(); // ['resources/js/panel/tables/DataTable.vue' => '…', …]
AssetManifest::compare(); // every shipped file, with a status2
3
4
5
6
Seluruh isi halaman ini pada dasarnya menjelaskan empat pemanggilan tersebut dan file yang mereka baca.
Mengapa file ini diperlukan
Frontend panel dipublikasikan ke dalam aplikasi, bukan diimpor langsung dari package. Ini merupakan trade-off yang disengaja — setiap component registry adalah allowlist import.meta.glob pada saat build terhadap tree milik aplikasi sendiri, dan komponen yang source-nya tidak dapat Anda baca juga tidak dapat Anda debug — tetapi konsekuensinya sama dengan sistem published asset lainnya: 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 | |
|---|---|
vendor:publish --tag=panda-panel-assets | Melewati semua file yang sudah ada. Tidak ada yang diperbarui. |
vendor:publish --tag=panda-panel-assets --force | Menimpa semuanya, termasuk perubahan yang memang sengaja dibuat. |
Keduanya tidak dapat membedakan file yang tertinggal dari file yang sengaja diedit, karena keduanya sama-sama "berbeda dari salinan milik package". Nilai yang hilang adalah nilai yang dalam Git disediakan oleh git merge-base: catatan tentang common ancestor.
Tiga hash, bukan dua
Dengan menyimpan hash yang dimiliki file pada saat dipublikasikan, tiga nilai dapat menjawab status file secara pasti:
| Di disk | Di package | Status | Yang dilakukan --update |
|---|---|---|---|
| = manifest | = manifest | current | tidak melakukan apa pun |
| = manifest | ≠ manifest | stale | menimpa dengan aman |
| ≠ manifest | = manifest | modified | dibiarkan |
| ≠ manifest | ≠ manifest | conflict | dilaporkan, tidak pernah disentuh |
| tidak ada | ada | deleted | dibiarkan |
| tidak ada di manifest, berbeda | ada | new | ditulis |
| tidak ada di manifest, identik | ada | current | tidak melakukan apa pun |
| ada di manifest | tidak lagi dikirim | removed-upstream | dilaporkan sebagai file yang dapat dihapus |
Hanya dua kategori yang ditulis otomatis, dan pada keduanya aplikasi terbukti belum memiliki keputusan sendiri terhadap file tersebut: satu belum pernah dimiliki dan satu belum pernah disentuh. Konflik hanya dilaporkan beserta path-nya dan tidak diselesaikan dengan tebakan otomatis — conflict memang merupakan diff yang perlu dibaca manusia. Penanganannya dibahas khusus di Menyelesaikan konflik asset.
Struktur file
{
"_": "Written by php artisan panel:install / panel:assets. Commit this file: it is the record of which version of the panel frontend this application published, and without it an upgrade cannot tell your edits from a stale copy.",
"files": {
"resources/css/panda-panel.css": "3f0a…",
"resources/js/panel/icons/registry.ts": "9c41…",
"resources/js/panel/tables/DataTable.vue": "b7e2…"
}
}2
3
4
5
6
7
8
| Lokasi | base_path('.panel-assets.json') — root aplikasi |
_ | Catatan untuk siapa pun yang membuka file. Diabaikan saat dibaca. |
files | Destination relatif terhadap aplikasi => content hash, diurutkan dengan ksort() |
| Hash | hash('xxh128', …) dari isi file, setelah \r\n diganti menjadi \n |
Commit file ini. Manifest merupakan catatan keputusan yang dibuat project, sama seperti composer.lock. Jika disimpan di bootstrap/cache, file akan dianggap dapat dibuat ulang dan kehilangan maknanya. Jika diletakkan di storage, file biasanya masuk .gitignore dan hilang pada deployment pertama.
Line ending dinormalisasi agar checkout Windows atau editor yang menulis CRLF tidak membuat seluruh file terlihat diedit. Report yang menganggap semua file conflict adalah report yang praktis tidak berguna.
PandaPanel\Support\Installer\AssetManifest
Sebuah class final berisi static method. Aman dipanggil dari tinker, test, maupun deploy script.
| Method | Signature |
|---|---|
path | public static function path(): string |
exists | public static function exists(): bool |
read | public static function read(): array<string, string> |
write | public static function write(array $existing = []): void |
compare | public static function compare(?array $files = null): array<string, array{status: string, destination: string, source: string|null}> |
path()
use PandaPanel\Support\Installer\AssetManifest;
AssetManifest::path(); // '/var/www/app/.panel-assets.json'2
3
Selalu mengembalikan base_path('.panel-assets.json'). Lokasi ini tidak configurable karena manifest yang lokasinya berubah-ubah justru menjadi file yang tidak dapat ditemukan tool secara konsisten.
exists()
if (! AssetManifest::exists()) {
// Nothing published, or published before the manifest existed.
}2
3
panel:assets menggunakannya untuk satu tujuan: menampilkan warning bahwa belum ada catatan yang dapat dipakai sebagai pembanding.
read()
$recorded = AssetManifest::read();
$recorded['resources/js/panel/tables/DataTable.vue'] ?? null; // 'b7e2…' or null
count($recorded); // how many files are recorded2
3
4
Mengembalikan hash yang tercatat dengan key berupa destination relatif terhadap aplikasi, dan mengembalikan [] jika file tidak ada. Manifest yang tidak dapat di-parse diperlakukan seperti manifest yang tidak ada, bukan sebagai fatal error. Dampak terburuknya adalah seluruh file terbaca sebagai new, yaitu keadaan yang sama dengan aplikasi yang memang belum pernah mempublikasikan asset.
Key atau value yang bukan string diabaikan, sehingga angka yang tidak sengaja tertulis ketika file diedit manual tidak merusak seluruh proses perbandingan.
write()
AssetManifest::write(); // record every shipped file on disk
AssetManifest::write(AssetManifest::read()); // keep what is already recorded, then record2
public static function write(array $existing = []): void| Parameter | Tipe | Default | Arti |
|---|---|---|---|
$existing | array<string, string> | [] | Hash yang harus dipertahankan untuk file yang tidak sedang ditulis. |
Ada dua karakteristik penting yang perlu dipahami:
- Method ini menghitung hash salinan milik aplikasi, bukan salinan package. Manifest mencatat apa yang dimiliki aplikasi. Jika sebuah file dipublikasikan lalu langsung diedit, hash yang tercatat adalah hash hasil edit tersebut. Merekam hash package akan mengklaim aplikasi memiliki salinan pristine yang sebenarnya tidak pernah dimilikinya.
- File yang masih dikirim package tetapi sudah tidak ada di disk akan dihapus dari catatan, sehingga file yang Anda hapus tidak lagi dilaporkan sebagai
deletedsetelah penulisan manifest berikutnya.
Parameter $existing menjelaskan mengapa panel:assets memanggil write(read()): entry untuk file yang sudah tidak dikirim package tetap dipertahankan, bukan hilang tanpa jejak.
compare()
use PandaPanel\Support\Installer\AssetManifest;
foreach (AssetManifest::compare() as $relative => $entry) {
if ($entry['status'] === AssetManifest::CONFLICT) {
echo $relative.PHP_EOL;
}
}2
3
4
5
6
7
public static function compare(?array $files = null): arrayMengembalikan satu entry untuk setiap file yang dikirim package, menggunakan destination relatif sebagai key dan dalam urutan yang konsisten:
AssetManifest::compare()['resources/js/panel/tables/DataTable.vue'];
// [
// 'status' => 'stale',
// '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
| Key | Tipe | Arti |
|---|---|---|
status | string | Salah satu dari tujuh constant yang dijelaskan di bawah |
destination | string | Path absolut di aplikasi |
source | string|null | Path absolut di package; null untuk removed-upstream |
Setiap entry yang masih tercatat di manifest tetapi tidak lagi dikirim package ditambahkan dengan status removed-upstream dan source bernilai null. File hanya dilaporkan, tidak dihapus, karena file yang berhenti dikirim package bisa saja sudah diadopsi menjadi bagian permanen aplikasi.
Parameter $files — destination => source — tersedia agar seluruh status dapat diuji menggunakan fixture sementara. Repository ini sekaligus bertindak sebagai test application, sehingga dengan map asli kondisi "di disk" dan "di package" tidak pernah benar-benar berbeda dan dua kasus terpenting tidak dapat diuji:
// tests/Feature/Panel/AssetUpgradeTest.php, in essence
$map = ['/tmp/app/Component.vue' => '/tmp/package/Component.vue'];
expect(AssetManifest::compare($map)['/tmp/app/Component.vue']['status'])
->toBe(AssetManifest::STALE);2
3
4
5
Pada aplikasi normal, jangan mengirim parameter apa pun.
Tujuh status
Status tersedia sebagai public constant sehingga kode dapat membandingkan constant, bukan menulis string secara manual:
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
| Constant | Value | Arti | Label pada report | --update | --force |
|---|---|---|---|---|---|
NEW | new | File yang belum pernah dipublikasikan aplikasi | new, hijau | menulis | menulis |
CURRENT | current | Sudah dipublish, tidak diedit, dan tidak berubah di upstream | current, abu-abu | tidak | tidak |
STALE | stale | Sudah dipublish, tidak diedit, tetapi berubah di upstream | out of date, kuning | menulis | menulis |
MODIFIED | modified | Sudah dipublish lalu diedit di aplikasi | yours, biru | tidak | menulis |
CONFLICT | conflict | Diedit di aplikasi dan berubah di upstream | CONFLICT, merah | tidak | menulis |
DELETED | deleted | Sudah dipublish lalu dihapus dari aplikasi | deleted by you, abu-abu | tidak | tidak |
REMOVED_UPSTREAM | removed-upstream | Pernah tercatat, tetapi tidak lagi dikirim package | no longer shipped, abu-abu | tidak | tidak |
--force hanya memperluas kategori yang boleh ditulis ke MODIFIED dan CONFLICT. File yang sengaja dihapus tetap terhapus, dan file yang tidak lagi dikirim package tidak dihidupkan kembali.
Apa saja yang tercatat di manifest
Persis sama dengan publish map, satu entry per file. PandaPanel\Support\Installer\PublishedAssets adalah satu-satunya tempat map tersebut didefinisikan. Pemanggilan publishes() pada service provider dan panel:assets sama-sama membacanya, karena dua salinan daftar akan mulai berbeda saat sebuah directory baru ditambahkan. Gejala drift seperti itu adalah file yang berhasil dipublish tetapi tidak pernah dilaporkan sebagai out of date.
| Method | Signature | Mengembalikan |
|---|---|---|
map | public static function map(): array<string, string> | source => destination absolut, format yang dibutuhkan vendor:publish |
files | public static function files(): array<string, string> | destination => source absolut, satu entry per file |
relative | public static function relative(string $path): string | destination tersebut dalam bentuk yang ditampilkan di report |
use PandaPanel\Support\Installer\PublishedAssets;
count(PublishedAssets::map()); // 7 — six directories and one stylesheet
count(PublishedAssets::files()); // every file inside them
PublishedAssets::relative('/var/www/app/resources/js/panel/tables/DataTable.vue');
// 'resources/js/panel/tables/DataTable.vue'2
3
4
5
6
7
Kedua map sengaja memiliki arah yang berlawanan: map() mengikuti format yang dibutuhkan vendor:publish, sedangkan files() mengikuti format yang dibutuhkan proses perbandingan. files() berisi file, bukan directory, karena pertanyaan "apakah ini up to date" adalah pertanyaan per file. Sebuah directory yang mendapat satu komponen baru dan memiliki satu komponen lain yang diedit tidak bisa disebut sekadar "berubah" atau "tidak berubah".
| Source di package | Destination di aplikasi |
|---|---|
resources/js/panel | PandaPanel\Support\FrontendPaths::panel() — default resources/js/panel |
resources/js/components | resources/js/components |
resources/js/composables | resources/js/composables |
resources/js/lib | resources/js/lib |
resources/js/pages | resources/js/pages |
resources/js/types | resources/js/types |
resources/css/panda-panel.css | resources/css/panda-panel.css |
Map dibuat ulang pada setiap pemanggilan, bukan disimpan sebagai constant, karena dua destination dapat dikonfigurasi. Membaca config saat class didefinisikan dapat membekukan value yang kebetulan aktif pada saat package discovery:
// config/panda-panel.php
'frontend' => [
'panel_path' => 'js/panel',
'pages_path' => 'js/pages/Panels',
],2
3
4
5
Yang tidak masuk manifest dan karena itu tidak pernah dibandingkan atau ditulis oleh mekanisme ini: file milik Anda sendiri di dalam directory hasil publish, config/panda-panel.php, migration, generator stub (yang memiliki publish tag terpisah), output Wayfinder resources/js/routes dan resources/js/actions, serta app.ts, vite.config.ts, dan resources/views/app.blade.php yang sepenuhnya milik aplikasi.
Siapa yang menulis manifest dan kapan
| Command | Menulis manifest |
|---|---|
php artisan panel:install | Selalu, tepat setelah config dan asset dipublikasikan |
php artisan panel:assets | Tidak pernah — pemanggilan tanpa opsi hanya meminta report |
php artisan panel:assets --update | Hanya jika setidaknya satu file berhasil ditulis |
php artisan panel:assets --force | Sama |
// PandaPanel\Console\Commands\PanelAssetsCommand::handle()
AssetManifest::write(AssetManifest::read());2
3
Manifest ditulis setelah proses copy selesai dan file dibaca kembali dari disk, karena manifest mencatat kondisi yang benar-benar dimiliki aplikasi, bukan apa yang command berniat tulis.
Mencatat hash hanya sebagai side effect dari membaca report akan membuat hasil eksekusi berikutnya bergantung pada apakah report pernah diminta. Karena itu panel:assets tanpa opsi tidak pernah menulis apa pun.
Membuat manifest untuk aplikasi yang diinstal sebelum fitur ini tersedia
Tanpa manifest, eksekusi pertama tidak memiliki baseline untuk dibandingkan. Command menjelaskan kondisi ini dan tetap melanjutkan:
WARN No .panel-assets.json, so there is no record of what this application published.
Everything already identical to the package reads as current; anything else reads as new.
Run --update to write one.2
3
Fallback tersebut disengaja: file yang belum tercatat tetapi sudah identik dengan salinan package dibaca sebagai current, bukan new, sehingga aplikasi dengan ratusan file identik tidak diarahkan untuk menimpa file yang sebenarnya sudah benar.
php artisan panel:assets --update # writes the manifest, if it wrote at least one fileJika seluruh file sudah identik, tidak ada file yang ditulis dan manifest tidak akan terbentuk. Buat manifest tanpa syarat dengan menjalankan pemeriksaan installer:
php artisan panel:install --no-panel --no-user --no-interactionSetelah itu, commit file manifest.
Menggunakannya dalam script Anda sendiri
use PandaPanel\Support\Installer\AssetManifest;
$report = AssetManifest::compare();
$counts = array_count_values(array_column($report, 'status'));
printf(
"%d out of date, %d yours, %d conflicts\n",
$counts[AssetManifest::STALE] ?? 0,
$counts[AssetManifest::MODIFIED] ?? 0,
$counts[AssetManifest::CONFLICT] ?? 0,
);2
3
4
5
6
7
8
9
10
11
12
panel:assets selalu keluar dengan exit code 0, termasuk ketika conflict ditemukan, karena menggagalkan deploy hanya karena ada file yang sengaja diedit bukan perilaku default yang tepat. Jika CI Anda ingin gagal pada conflict, gunakan report di atas lalu tambahkan exit sesuai kebutuhan.
Hal yang perlu diperhatikan
- Commit
.panel-assets.json. Tanpa file ini, setiap upgrade berikutnya berubah menjadi pilihan antara menimpa perubahan Anda atau tidak memperbarui apa pun. - Menghapus manifest dapat dipulihkan, tetapi kehilangan informasi. Semua file yang identik dengan package terbaca sebagai
current, sedangkan semua file lain terbaca sebagainew; artinya perubahan Anda dapat terlihat seperti file baru yang kemudian ditimpa oleh--update. - Hash berasal dari salinan Anda, bukan salinan package. Publish lalu edit dalam satu sesi akan mencatat hasil edit sebagai baseline. Itu jawaban yang benar, meskipun kadang mengejutkan.
panel:iconsmembuat sebuah published file menjadi milik aplikasi. Command tersebut menulis ulangresources/js/panel/icons/registry.tsberdasarkan icon yang dideklarasikan panel Anda, sehingga file itu akan terbacamodifiedsetelahnya. Jalankan ulangphp artisan panel:iconssetelah setiap--force.- File yang Anda hapus dapat muncul kembali pada akhirnya.
deletedtidak pernah ditulis oleh--update, tetapi penulisan manifest berikutnya akan menghapus catatannya. Pada eksekusi setelah itu file terbaca sebagainewdan dapat ditulis kembali. - Entry
removed-upstreambersifat sticky. Command mempertahankannya melaluiwrite(read()). Hapus entry terkait dari JSON jika Anda tidak ingin status tersebut terus ditampilkan. - Repository ini sendiri selalu terbaca sebagai
current. Published copy di repository memang merupakan source milik package. Itu sebabnyacompare()menerima map yang dapat diinjeksi untuk kebutuhan test. - Perbedaan line ending bukan sebuah edit. Hash menormalisasi
\r\nmenjadi\nsebelum perhitungan.
Lihat juga
- Menyelesaikan konflik asset — kasus yang tidak dapat diselesaikan manifest secara otomatis
- Panduan upgrade, Breaking changes
- Kebijakan versioning — alasan frontend memiliki versinya sendiri
panel:assets,panel:install, Publish tags- Memperbarui asset hasil publish, struktur asset hasil publish
- Frontend paths, Service provider
- Frontend assets, Component registries
- Frontend build
- Troubleshooting: konflik asset