Local Development
Getting a checkout of chocoalano/panel to the point where the verification loop runs. The package is two halves that are checked by two different toolchains — PHP under src/, and Vue and TypeScript under resources/js/ — and neither one can say anything about the other, so a working setup means both. This page is the setup, the commands, and the parts of a checkout that are not obvious from looking at it.
A minimal working example
git clone https://github.com/chocoalano/panda-panel.git
cd panda-panel
composer install
npm ci
composer ci # pint --test, then phpstan, then pest
npm run ci # prettier --check, then eslint, then vue-tsc, then vite build2
3
4
5
6
7
8
Both scripts are declared in the repository rather than assembled here, and both are what CI runs. A change that passes them locally fails CI only for a version-specific reason — which is what the CI matrix is for.
There is no .env to write, no database server to start, and no php artisan step. The suite runs on sqlite :memory: and builds its own application; the next two sections are why.
What is in a checkout
src/ the framework — PandaPanel\*, PSR-4 from composer.json
config/ config/panda-panel.php
database/ the package's own migrations
stubs/ generator scaffolding for make:panel*
resources/ css, js — the Vue frontend an application publishes
examples/ the application the test suite runs against
tests/ the suite
frontend/ host stand-ins and the compile-check entry
docs/ this documentation
build/ scratch output — gitignored2
3
4
5
6
7
8
9
10
The first five directories are the shipped package. Everything else is how the repository is developed and is export-ignored in .gitattributes, so composer require chocoalano/panel never brings it. See Releases for what that list is, and for the one file that looks like a development file and is deliberately kept off it.
The two toolchains
PHP
composer.json declares six scripts. They are the only PHP entry points worth memorising:
| Script | Runs |
|---|---|
composer test | vendor/bin/pest |
composer test-coverage | vendor/bin/pest --coverage |
composer format | vendor/bin/pint |
composer format-check | vendor/bin/pint --test |
composer analyse | vendor/bin/phpstan analyse --memory-limit=1G |
composer ci | @format-check, then @analyse, then @test |
composer format # fix style
composer analyse # larastan level 4 over src and database
composer test # the whole suite
composer ci # all three, in the order a failure is most useful2
3
4
composer ci is ordered deliberately: a style failure is a one-command fix, a static-analysis failure names a line, and a test failure is the one that takes reading. Running them the other way round means finding out about a missing declare(strict_types=1) after a minute of tests.
Frontend
package.json declares seven scripts:
| Script | Runs |
|---|---|
npm run lint | eslint resources/js frontend --max-warnings=0 |
npm run lint:fix | eslint resources/js frontend --fix |
npm run format | prettier --write resources/js frontend resources/css |
npm run format:check | prettier --check resources/js frontend resources/css |
npm run typecheck | vue-tsc --noEmit -p tsconfig.json |
npm run build | vite build |
npm run ci | format:check, lint, typecheck, build |
npm ci # install from the committed lockfile
npm run format # fix formatting
npm run lint:fix # fix what eslint can fix
npm run typecheck # vue-tsc over every component
npm run build # does all of it compile together
npm run ci # all four, as CI runs them2
3
4
5
6
engines declares "node": ">=20.19". CI runs Node 20, 22 and 24; anything older is untested and the @tailwindcss/vite and Vite 7 versions in dependencies will not resolve.
npm run build produces nothing anybody ships. It exists to answer the one question type-checking cannot: whether every file in the tree resolves and compiles together. Frontend toolchain is the whole of that story.
The test application
The package has no bootstrap/app.php of its own, so the suite builds an application with orchestra/testbench and points it at examples/:
// tests/TestCase.php
protected function resolveApplicationConfiguration($app): void
{
$app->useAppPath((string) realpath(__DIR__.'/../examples/app'));
$app->useDatabasePath((string) realpath(__DIR__.'/../examples/database'));
$app->useBootstrapPath((string) realpath(__DIR__.'/../vendor/orchestra/testbench-core/laravel/bootstrap'));
$app->useStoragePath(dirname(__DIR__).'/build/testbench/storage');
parent::resolveApplicationConfiguration($app);
}2
3
4
5
6
7
8
9
10
11
examples/ holds App\Models\User, App\Panels\Admin, App\Panels\App, the policies, the factories, the routes and the Inertia root view. It is autoloaded under App\ through autoload-dev, so it exists for development and never ships. Using the examples as the test application means they are exercised by the suite rather than left to rot beside it: every snippet in these docs that names UserResource names a class the suite actually runs.
applicationBasePath() points base_path() and resource_path() at the package root, because three things the panel reads through those helpers are real files in this repository — the icon registry, the generator stubs, and the TypeScript the serialized schemas are checked against.
Directories a run creates
Git cannot commit an empty directory, and a .gitkeep in each would be seven files whose only job is to exist. TestCase::prepareWritableDirectories() makes them on first use, called from tests/Pest.php before the first application is built:
bootstrap/cache Laravel's package manifest
resources/views the view finder globs it
build/testbench/storage/app/private the disk exports land on
build/testbench/storage/framework/views
build/testbench/storage/framework/cache/data
build/testbench/storage/framework/sessions
build/testbench/storage/logs2
3
4
5
6
7
Two of them have to sit under the base path rather than under build/: Laravel writes its package manifest while the application is still being constructed, and route:cache rebuilds the application in a process that never sees TestCase. Neither is shipped — the package has no Blade views of its own — and both are gitignored.
A clean is one command:
rm -rf build bootstrap resources/viewsbuild/ also holds build/phpstan (the analyser's tmpDir) and build/frontend (the Vite output). Deleting it costs a slower next run and nothing else.
Running one thing at a time
Pest takes a path, a filter, or both:
vendor/bin/pest tests/Feature/Panel/ResourceQueryTest.php
vendor/bin/pest --filter=ResourceUrl
vendor/bin/pest --filter='refuses a member the index'
vendor/bin/pest --compact # one character per test
vendor/bin/pest --bail # stop at the first failure
vendor/bin/pest --dirty # only files with uncommitted changes
vendor/bin/pest --coverage # needs Xdebug or PCOV2
3
4
5
6
7
Pint takes paths and has three modes:
vendor/bin/pint # fix everything not excluded
vendor/bin/pint src tests # fix these paths only
vendor/bin/pint --test # report, change nothing — what CI runs
vendor/bin/pint --dirty # only files with uncommitted changes
vendor/bin/pint --diff=main # only files changed since branching off main
vendor/bin/pint -v # name every rule that fired2
3
4
5
6
PHPStan reads phpstan.neon from the repository root and needs no arguments:
vendor/bin/phpstan analyse
vendor/bin/phpstan analyse --memory-limit=1G # what composer analyse runs
vendor/bin/phpstan analyse --no-progress # what CI runs
rm -rf build/phpstan # discard the result cache2
3
4
The frontend scripts take no arguments, but the underlying tools do:
npx eslint resources/js/panel/tables --max-warnings=0
npx prettier --check resources/js/panel/forms
npx vue-tsc --noEmit -p tsconfig.json2
3
Exercising an artisan command
There is no artisan binary in this repository, because there is no application here to run one against. Commands are exercised the way the suite exercises them, inside the Testbench application:
it('creates a panel provider and the directories discovery scans', function (): void {
$this->artisan('make:panel', ['name' => 'Testing'])->assertSuccessful();
expect(File::exists(app_path('Panels/Testing/TestingPanelProvider.php')))->toBeTrue();
});2
3
4
5
app_path() there is examples/app, so a generator run writes into the example application and GeneratorTest deletes it afterwards. examples/app/Panels/Testing is gitignored for exactly that reason.
GeneratorTest then runs Pint over what was generated, so a stub that stops complying with this repository's own style fails here rather than in the next person's project:
$pint = Process::run([base_path('vendor/bin/pint'), '--test', app_path('Panels/Testing')]);
expect($pint->successful())->toBeTrue($pint->output());2
3
That test skips itself when vendor/bin/pint is absent, so a --no-dev install does not fail it.
What git tracks
Three things in .gitignore surprise people:
composer.lock
.github
.ai
.claude
.codex2
3
4
5
composer.lockis not committed. A library's job is to work against the ranges incomposer.jsonrather than against one resolution of them, and CI resolves fresh on every run — with--prefer-lowestas well as--prefer-stable.composer installin a fresh clone therefore resolves rather than installing from a lock, which is correct and is also why two checkouts can hold different patch versions.package-lock.jsonis committed, and CI runsnpm ciagainst it. That is the opposite decision for the opposite reason: this repository's own toolchain has to be reproducible, while an application installs from the ranges and never sees the lockfile..github/is ignored. The workflow file exists in a working tree and is not tracked, so an edit to it needsgit add -f .github/workflows/tests.ymlto reach a commit.
Notes
composer installon a fresh clone prints "No lock file found" and resolves instead. That is the intended state, not a broken checkout.- The suite needs
pdo_sqliteandzip. sqlite because the harness runs on:memory:, zip because the xlsx writer is real rather than mocked. Both are in the CIsetup-phpstep for the same reason. - No PHP test depends on
npm run build.Illuminate\Foundation\Viteis replaced withTests\Fixtures\Panel\FakeViteindefineEnvironment(), so a checkout with nonode_modulesstill runs the whole PHP suite. - A test that registers a fixture panel must guard with
PanelManager::has(). The registry survives between tests in one process, and registering twice throws. vendor/bin/pest --coverageneeds a coverage driver. CI setscoverage: noneon every PHP job, so coverage is a local-only tool here.- Deleting
build/deletes the Testbench storage. The suite recreates it; nothing in there is worth keeping.
See also
- Running the tests — the harness, the fixtures, and where a new test goes
- Frontend toolchain — what each config file decides
- Coding standards — what Pint and PHPStan enforce, and the two traps
- Pull requests — the checklist before opening one
- Releases — versioning, the changelog, and the export list
- CI matrix — how these commands are combined in GitHub Actions
- Directory structure — the same tree, from an application's side
- Test setup — the helpers, and testing a panel in your own project