Toolchain Frontend
Setengah lainnya dari package ini adalah Vue dan TypeScript di bawah resources/js/. Tidak ada job PHP yang dapat memastikan bagian ini benar. Empat file config menentukan cara frontend diperiksa — package.json, eslint.config.js, tsconfig.json, dan vite.config.ts — dan hanya file pertama yang ikut distribusi karena installer membacanya saat runtime. Halaman ini menjelaskan apa yang diputuskan masing-masing config dan mengapa, karena build di repository package membuktikan hal yang berbeda dari build di application pengguna.
Contoh minimal
npm ci
npm run ci2
npm run ci menjalankan format:check, lalu lint, kemudian typecheck, dan terakhir build. Urutannya sengaja demikian karena type error biasanya memberi pesan yang lebih jelas dibanding error bundler untuk masalah yang sama. Ini adalah command yang dijalankan blocking CI job pada Node 20, 22, dan 24.
Scripts
Tujuh script dari package.json:
| Script | Command |
|---|---|
lint | eslint resources/js frontend --max-warnings=0 |
lint:fix | eslint resources/js frontend --fix |
format | prettier --write resources/js frontend resources/css |
format:check | prettier --check resources/js frontend resources/css |
typecheck | vue-tsc --noEmit -p tsconfig.json |
build | vite build |
ci | npm run format:check && npm run lint && npm run typecheck && npm run build |
npm run format # memperbaiki formatting
npm run lint:fix # memperbaiki hal yang dapat di-fix ESLint
npm run typecheck # menjalankan vue-tsc pada seluruh component
npm run build # memastikan semuanya dapat dikompilasi bersama2
3
4
--max-warnings=0 berarti warning juga dianggap failure. Tidak ada kategori warning yang diabaikan CI.
engines menetapkan floor Node yang sama dengan awal matrix CI:
"engines": { "node": ">=20.19" }Package ini bukan npm package
{
"name": "@chocoalano/panel",
"version": "0.0.0",
"private": true
}2
3
4
5
Dengan "private": true dan version 0.0.0, package ini tidak pernah dipublish ke npm. Component mencapai application melalui vendor:publish, lalu dibuild oleh Vite milik application menggunakan dependency tree application tersebut. package.json di repository ini berfungsi untuk mendeklarasikan dependency yang dibutuhkan published component sekaligus menyimpan scripts untuk memeriksanya.
dependencies vs devDependencies adalah sebuah contract
dependencies berarti apa yang wajib diinstall application agar published component dapat dibuild. devDependencies berarti apa yang dibutuhkan repository ini untuk memeriksa source-nya. Perbedaan ini ditegakkan melalui kode, bukan hanya kebiasaan:
use PandaPanel\Support\Installer\FrontendRequirements;
FrontendRequirements::npmPackages(); // list<string> pasangan 'name@range'
FrontendRequirements::missingNpmPackages(); // daftar yang sama dikurangi dependency yang sudah dideklarasikan application2
3
4
public static function npmPackages(): array
public static function missingNpmPackages(): array2
npmPackages() membaca block dependencies dari package.json milik repository ini, tepatnya dirname(__DIR__, 3).'/package.json'. Daftar tidak ditulis ulang di PHP karena copy kedua akan drift saat sebuah component mengimpor dependency baru. php artisan panel:install menggunakan daftar ini untuk mencetak command npm install … yang benar.
Karena itu setiap import baru pada published component harus disertai dependency yang sesuai di dependencies. InstallerTest memastikan source daftar dan hasil helper tetap sinkron:
$declared = json_decode(File::get(dirname(__DIR__, 3).'/package.json'), true)['dependencies'];
expect(FrontendRequirements::npmPackages())->toHaveCount(count($declared));2
3
missingNpmPackages() membandingkan terhadap package.json milik application, baik dependencies maupun devDependencies. Yang penting adalah apakah project mendeklarasikan dependency tersebut. Copy transitif yang kebetulan ada di node_modules hari ini dapat hilang ketika dependency lain diupgrade.
Host seam
Published component mengimpor sembilan belas module yang memang tidak disediakan package. frontend/host/ menyediakan stand-in minimal untuk semuanya dan hanya digunakan saat type-check serta build repository ini:
frontend/host/
├── actions/ controller action yang dihasilkan Wayfinder
├── components/ Heading, UserInfo, UserMenuContent, PasskeyItem, …
├── composables/ useTwoFactorAuth
├── routes/ route module yang dihasilkan Wayfinder
└── types/ shared type application dan types/ui2
3
4
5
6
Dua kelompok module memang mustahil dikirim package, sedangkan sisanya justru salah jika dipaksakan. routes/* dan actions/* dihasilkan Wayfinder dari route table application; copy di package hanya akan menjadi snapshot route milik project lain. Component seperti UserMenuContent juga merupakan bagian dari desain application karena project sendiri yang menentukan account link yang ingin ditampilkan.
Stub mendeklarasikan props, emits, dan exports secara tepat sesuai yang digunakan Panel component, tanpa any pada boundary tersebut. Jika stub drift dari starter kit yang sebenarnya, build repository gagal lebih awal daripada application pengguna.
Daftar yang diperiksa panel:install disimpan pada FrontendRequirements:
public static function missingHostModules(): array // list<string> specifier '@/…'
public static function hasVite(): bool
public static function missingInertia(): array // penjelasan apa yang hilang
public static function layoutOverrides(): array // list<array{file: string, line: int, code: string}>2
3
4
Sebuah specifier dicari dengan enam bentuk: .ts, .vue, .d.ts, /index.ts, /index.vue, /index.d.ts. Dulu pencarian juga mencoba suffix kosong '', yang menyebabkan entry berbentuk directory selalu dianggap tersedia karena File::exists() juga true untuk directory. Akibatnya @/types dapat dianggap terpenuhi hanya karena folder yang dipublish package ada, padahal module sebenarnya belum tersedia.
@/ di-resolve dalam dua tahap
@/x berarti cari resources/js/x terlebih dahulu, lalu fallback ke frontend/host/x. Urutannya penting: module yang benar-benar dikirim package harus resolve ke source package, sedangkan hanya sembilan belas seam yang tidak dikirim yang boleh jatuh ke host stub.
TypeScript mendapat aturan ini dari ordered paths:
"paths": {
"@/*": ["./resources/js/*", "./frontend/host/*"]
}2
3
Vite tidak dapat mengekspresikan fallback ini dengan dua alias karena alias kedua akan tertutup oleh yang pertama. Karena itu vite.config.ts mengimplementasikan strategi sama melalui plugin:
const SOURCE_ROOTS = ['resources/js', 'frontend/host'] as const;
const EXTENSIONS = ['', '.ts', '.vue', '/index.ts', '/index.vue'] as const;
function hostSeam(): Plugin {
return {
name: 'panda-panel:host-seam',
enforce: 'pre',
resolveId(source) {
if (!source.startsWith('@/')) {
return null;
}
const relative = source.slice(2);
for (const base of SOURCE_ROOTS) {
for (const extension of EXTENSIONS) {
const candidate = resolve(root, base, `${relative}${extension}`);
if (isFile(candidate)) {
return candidate;
}
}
}
return null;
},
};
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Check isFile() adalah bagian penting. Empty extension dicoba lebih dulu agar @/lib/utils.ts dapat di-resolve apa adanya. Tetapi @/components/ui/button adalah directory yang benar-benar ada dan specifier tersebut bermaksud menunjuk index.ts. Jika sekadar memeriksa "path exists", barrel import akan dianggap sebagai file.
TypeScript dan Vite menggunakan urutan source yang sama sehingga bundler dan type-checker tidak memiliki interpretasi berbeda. Mengubah salah satunya berarti harus mengubah sisi lain juga.
Build adalah compile check
Tidak ada hasil npm run build yang ikut distribusi. Tujuannya menjawab pertanyaan yang tidak dapat dibuktikan type-check saja: apakah semua file benar-benar resolve dan dapat dikompilasi bersama?
export default defineConfig({
plugins: [hostSeam(), vue(), tailwindcss()],
build: {
outDir: 'build/frontend',
emptyOutDir: true,
minify: false,
rollupOptions: {
input: resolve(root, 'frontend/entry.ts'),
},
},
});2
3
4
5
6
7
8
9
10
11
| Option | Value | Alasan |
|---|---|---|
outDir | build/frontend | Berada di build/ yang di-gitignore; rm -rf build menjadi clean lengkap. |
emptyOutDir | true | Artifact dari component yang sudah dihapus tidak tertinggal. |
minify | false | Tidak ada manfaat mengecilkan artifact yang akan langsung dibuang. |
input | frontend/entry.ts | Generated, bukan authored manual. |
Entry melakukan glob terhadap seluruh tree, bukan daftar component manual:
const modules = {
...import.meta.glob('../resources/js/**/*.vue', { eager: true }),
...import.meta.glob('../resources/js/**/*.ts', { eager: true }),
};
export default Object.keys(modules).sort();
import '../resources/css/panda-panel.css';2
3
4
5
6
7
8
eager: true memaksa Rollup me-resolve, mem-parse, dan mengompilasi setiap module. Lazy chunk hanya akan memindahkan failure ke runtime browser yang bahkan tidak pernah dijalankan build ini. Key diexport agar module tidak di-tree-shake sebelum diperiksa. Stylesheet ikut diimport karena Tailwind 4 menyimpan theme, custom variant, dan @source scan di CSS; stylesheet rusak sama seriusnya dengan component rusak.
Daftar import yang ditulis manual hanya memeriksa file yang diingat developer. Build yang secara diam-diam berhenti mencakup file baru lebih buruk daripada tidak ada build check sama sekali.
Lokasi file
@inertiajs/vite hanya melakukan glob pada resources/js/pages/**, sehingga struktur directory menjadi:
| Directory | Isi |
|---|---|
resources/js/panel/** | Building blocks, tidak pernah menjadi Inertia Page |
resources/js/pages/panel/** | Generic Inertia Page milik framework |
resources/js/pages/Panels/{Panel}/** | Page dan custom Widget milik application |
resources/js/components/ui/** | Component yang berasal dari shadcn-vue |
resources/js/composables, lib, types | Shared helper dan shared prop types |
resources/js/components/ui/** dibiarkan memakai formatting shadcn-vue. Prettier mengabaikannya dan ESLint melonggarkan beberapa rule karena application sangat mungkin menarik ulang file-file tersebut dari upstream. Namun file tetap di-typecheck dan dibuild, karena kedua pemeriksaan itulah yang menangkap breakage nyata.
Registry saat build
Icon dan custom Widget component hanya dapat di-resolve melalui registry yang dibuat saat compile. Nama yang tidak ikut compile tidak dapat dijangkau walaupun request mengirim nama tersebut.
php artisan panel:icons # menulis ulang resources/js/panel/icons/registry.ts dari source
php artisan panel:icons --check # gagal tanpa menulis, cocok untuk CI2
Jangan edit registry.ts manual. Header file juga menyatakan hal tersebut. Untuk menambah icon, gunakan nama Lucide di PHP lalu jalankan command. Nama divalidasi terhadap node_modules/@lucide/vue/dist/esm/icons/*.mjs, sehingga typo gagal dengan nama yang jelas daripada menghasilkan button tanpa icon.
Dua aturan penting ketika membuat registry:
- Jangan gunakan alias
@padaimport.meta.glob. Dev server Vite dapat menghasilkan glob kosong untuk aliased pattern sementara production build bekerja. Gunakan relative pattern seperti'../../pages/Panels/**/Widgets/*.vue'. - Bangun lookup key dari real path. Format key mengikuti pattern dan dapat berbeda antara dev (
../../pages/...) dan build (./pages/...). MapObject.entries(modules)daripada merekonstruksi path dari component name.
Toolchain ini tidak ikut distribusi
.gitattributes memberi export-ignore pada seluruh file berikut:
/frontend export-ignore
/package-lock.json export-ignore
/tsconfig.json export-ignore
/vite.config.ts export-ignore
/eslint.config.js export-ignore
/.prettierrc.json export-ignore
/.prettierignore export-ignore2
3
4
5
6
7
Application menerima published component dan membuild-nya dengan toolchain sendiri menggunakan version ranges, bukan lockfile repository package. Karena itu config file baru di repository root juga harus mendapat export-ignore.
Satu file yang terlihat seperti development-only tetapi sengaja tidak di-ignore adalah package.json. FrontendRequirements membacanya saat runtime dari dalam vendor/ untuk mengetahui npm package yang diperlukan published component. Ketika file ini pernah di-export-ignore, panel:install tidak melempar error; check justru menghasilkan empty list dan terlihat seperti semua dependency sudah tersedia. Negative/DistributionTest kini melindungi behavior ini. Detailnya ada di Releases.
Hal yang perlu diperhatikan
npm cigagal jika lockfile berbeda daripackage.json, bukan melakukan resolve ulang. Itu behavior yang diinginkan karena range yang diubah tanpa regenerasi lockfile berarti perubahan tersebut belum benar-benar diuji.- Lockfile committed tetapi tidak pernah dikirim ke application. Blocking CI memakai
npm ciuntuk reproducibility. Job non-blocking terpisah memakainpm install --no-package-lockuntuk mensimulasikan version range terbaru yang benar-benar akan ditemukan application. - Panel asset entrypoint baru selalu dua edit.
Panel::assets(...)menambahkan entrypoint ke Vite milik application, dan path yang sama harus masukvite.config.tsrepository agar compile check juga mencakupnya. - Jangan interpolation Tailwind class.
md:col-span-${n}tidak pernah terlihat scanner Tailwind dan dapat hilang dari bundle. Gunakan literal mapping. - Deferred Inertia prop harus optional di Vue.
Inertia::defer()membuat prop tidak ada pada response pertama, bukan bernilai null. Required Vue prop akan menghasilkan warning pada first paint. vue/multi-word-component-namessengaja dimatikan. Nama sepertiDataTabledanActionModaladalah vocabulary framework.build/frontendikut terhapus olehrm -rf build, bersama PHPStan cache dan Testbench storage. Tidak ada artifact penting di dalamnya.
Lihat juga
- Local development — setup dan kedua toolchain berdampingan
- Coding standards — config ESLint, Prettier, dan TypeScript per rule
- Releases — export list dan file yang wajib masuk dist
- Running the tests — termasuk test terhadap frontend files
- Host modules — sembilan belas host seam dari sisi application
- Frontend assets dan assets
- Component registries
- Icons dan
panel:icons - CI matrix — dua frontend job dan apa yang dibuktikan masing-masing