Skip to main content

Dev Orchestrator

bun run dev starts the entire development stack in a coordinated sequence with colored terminal output, including code generation from your Laravel routes.

Startup Sequence

bun run dev
|
1. Load .env + resonance.config.ts
|
2. Start Laravel server (php artisan serve --port=8880)
3. Poll /up until 200 <- server must be up for generation
|
4. Run code generation (bunx kubb generate)
5. Wait 2s <- let filesystem writes settle
| before Vite starts watching
6. Start in parallel:
+---> Vite dev server
+---> Pail (log streaming)
+---> Queue worker (if enabled)
+---> Custom commands (from config)

Code Generation Pipeline

This is how your Laravel routes become type-safe React Query hooks.

Scramble reads your Laravel routes and generates an OpenAPI 3.x spec. It uses the route name as the operationId. The spec is served at /docs/api.json while the dev server runs.

Kubb fetches that spec and runs four plugins in sequence:

Laravel Routes (named routes)
|
v
Scramble -> OpenAPI JSON spec (/docs/api.json)
|
v
Kubb (4 plugins: OAS -> TS -> Zod -> React Query)
|
v
.resonance/ output directory
|
+--> types/ (TypeScript interfaces)
+--> zod/ (Zod validation schemas)
+--> hooks/ (React Query hooks)
|
v
Vite HMR (hot reload in browser)

Route Name Convention

Hook names derive from Laravel route names. The conversion:

Route::post('/demo/tasks', 'store')->name('demo.tasks.store')
-> operationId: 'demo.tasks.store'
-> hook: useDemoTasksStore()

Rules: split by dots, PascalCase each segment, prepend use.

Route NameHTTP MethodGenerated Hook
loginPOSTuseLogin()
users.indexGETuseUsersIndex()
demo.tasks.indexGETuseDemoTasksIndex()
demo.tasks.storePOSTuseDemoTasksStore()
demo.tasks.destroyDELETEuseDemoTasksDestroy()
demo.profile.showGETuseDemoProfileShow()
demo.profile.updatePOSTuseDemoProfileUpdate()
demo.posts.indexGETuseDemoPostsIndex()
demo.posts.likePOSTuseDemoPostsLike()

GET routes produce useQuery hooks. POST/PUT/PATCH/DELETE routes produce useMutation hooks.

Generated Output

resources/js/.resonance/
├── types/
│ ├── DemoTasksIndex.ts
│ ├── DemoTasksStore.ts
│ └── ...
├── zod/
│ ├── demoTasksStoreRequest.ts
│ └── ...
└── hooks/
├── useDemoTasksIndex.ts
├── useDemoTasksStore.ts
└── ...

The directory is .gitignored. Generated types match your API's request and response shapes. Generated hooks wire to the kubb-client, which handles envelope parsing and signal processing.

The queryKey in each hook contains a { url: string } object -- this is how the invalidation system matches signals to cached queries.

Regeneration

After changing your API (adding routes, modifying return types, updating Form Requests), regenerate:

bun run generate

The server must be running -- Kubb fetches the spec from the live OpenAPI endpoint. Vite detects the updated files and triggers HMR.

clean: false in the Kubb config prevents deleting the output directory on each run (TanStack Router's routeTree.gen.ts lives nearby). The dev orchestrator adds a 2-second delay after generation to prevent partial-write HMR flicker.

Terminal Output

Each process gets a colored prefix:

[server]  INFO  Server running on [http://127.0.0.1:8880].
[vite] VITE v7.0.7 ready in 234 ms
[pail] ┌ 2024-01-15 14:23:01 INFO ─────────┐
[pail] │ Task created: Buy groceries │
[queue] [2024-01-15 14:23:01] Processing: App\Jobs\SendNotification

Process Management

Child processes run with piped stdio and FORCE_COLOR: '1' so tools like Vite and Artisan emit colored output through pipes.

If any service crashes, the entire stack shuts down -- running with a dead queue worker or broken Vite server leads to confusing partial failures. On Ctrl+C:

  1. SIGTERM to all children
  2. Wait 3 seconds for graceful exit
  3. SIGKILL anything still running

Configuration

// resonance.config.ts
export default {
server: {
host: '127.0.0.1',
port: 8880,
},
queue: true,
customCommands: [
{
name: 'horizon',
cmd: ['php', 'artisan', 'horizon'],
color: 'magenta',
},
],
kubb: {
openApiUrl: 'http://localhost:8880/docs/api.json',
outputPath: './resources/js/.resonance',
clientImportPath: '@jhavenz/resonance/client/kubb-client',
},
};

Config can also be a function receiving { mode, command }. The loader searches for resonance.config.ts, .js, or .mjs. If none exists, defaults apply.

Services

ServiceCommandPurpose
Laravel Serverphp artisan serveHosts app + OpenAPI spec endpoint
Code Generationbunx kubb generateOne-shot type/hook generation from spec
Vitebunx viteFrontend dev server with HMR
Pailphp artisan pailReal-time log streaming
Queue Workerphp artisan queue:workJob processing (optional)
CustomConfigurableHorizon, Reverb, project-specific tools