E - The editor · · 5 min read

Understanding the Space Between: What e 0.6.4 Is About

On the quiet gaps in a modern Laravel app — between PHP and JavaScript, between a class and its template, between "it worked in dev" and "why is this slow" — and how this release fills them.

Understanding the Space Between: What e 0.6.4 Is About

Most editor features live inside a single file. Syntax highlighting, completion, go-to-definition — they're about the file under your cursor. But the bugs that actually eat your afternoon almost never live in one file. They live in the space between two files that pretend to be one.

A controller sends props; a Vue component hopes they're the right shape. A Livewire class declares $name; a Blade view binds to it with wire:model="name" — and nothing keeps them in sync. A route runs beautifully in dev and fires forty queries you never see. That in-between space is invisible to almost every tool.

e 0.6.4 is about making it visible. Here's the how and the why, with examples.

Why Inertia was the big gap

e already understood both halves of an Inertia app: PHP and Laravel on one side, Vue/Svelte/TypeScript on the other, plus the live database schema. But the bridge between them — which is Inertia — was invisible. The architecture map, for instance, was built around Blade views, so an Inertia project showed you half an app: route → controller → …nothing.

That's a table-stakes miss for a Laravel editor. So the first thing was resolution.

It just resolves now

Inertia::render('Users/Index') now behaves exactly like view('users.index') always has:

return Inertia::render('Users/Index', [
    //                   └── ⌘-click jumps to resources/js/Pages/Users/Index.vue
    'users' => User::paginate(),
]);

Completion suggests your real page components, and the architecture map (⌘⌥M) now runs the whole way: route → controller → page component. Half an app became a whole one.

The killer: the props contract (⌘⌥C)

Here's the problem nobody had solved cleanly. The controller sends ['users' => User::paginate(), 'filters' => …], and the page component just... trusts it. People bolt on spatie/typescript-transformer, Wayfinder, or hand-rolled typegen scripts to paper over it.

e can do it with zero package setup, because it already knows your Eloquent models and the live schema. Open a page component and press ⌘⌥C:

Props Contract — Users/Index
controller: app/Http/Controllers/UserController.php

users: User[] ✓ filters: unknown ⚠ sent but unused role ⚠ used but never sent

It found the controller, inferred that User::paginate() is a User[], noticed the component never touches filters, and — the other direction — noticed the component reads props.role that the controller never sends. That second one is the bug that ships to production and blows up on someone's laptop.

And Generate TypeScript writes real interfaces, expanding each model from the actual database columns:

export interface UsersIndexProps {
users: User[];
filters: unknown;
}

export interface User { id: number; email: string; bio?: string; // nullable column → optional }

No transformer, no schema file to maintain. It's the one thing no editor or LSP does today, because it needs PHP, TypeScript, and the database understood at the same time — which is exactly the combination e sits on.

The natural extensions

Once the bridge exists, the rest falls out of it.

Ziggy on the JS side. route('users.show') in your Vue/Svelte files gets completion, hover, and go-to-definition — from the same route table the PHP side uses:

router.visit(route('users.show', user.id))
//                  └── hover shows GET /users/{user} → UserController@show

Shared props. HandleInertiaRequests::share() is parsed, so the "global" props that were invisible to tooling finally complete:

$page.props.auth.user   // ← completes, because e read share()

Inertia-aware replay. Replaying a route used to show raw HTML. Now it shows the Inertia payload as an explorable tree, component name on top:

⚛ Users/Index
props
users  [15 items]
[0]
id: 1
email: "ada@example.com"

The form contract. useForm({ name: '', email: '' }) checked against the matching FormRequest's rules — following form.post(route('users.store')) all the way to StoreUserRequest::rules():

form → users.store  (StoreUserRequest)
phone   ⚠ not validated
terms   ⚠ validated but not in the form

Forms are 80% of CRUD, and this catches the exact mismatches that silently drop fields.

While we were at it: Livewire and runtime

Two more in-between spaces got filled.

Livewire treats the class and the Blade view as the one thing they pretend to be. wire:model completes from the class's public properties, ⌘⌥J flips between the two files, and the crown jewel — renaming a property with F2 updates both the class and every wire: reference in the view:

public $email;          →  public $address;   (in the class)
<input wire:model="email">  →  wire:model="address"   (in the view)

One rename, both files, and it leaves unrelated strings alone.

Runtime insight (⌘⌥I) is a Telescope-style panel that captures every request against your dev app via Clockwork while you work:

POST /orders   201   84ms   Q:23 ⚠×19   C:2/1   ✉1   ⚡3   ✨

That Q:23 ⚠×19 is an N+1 caught in the act — one query ran nineteen times. ✨ hands the whole request to your agent. No Telescope, no Debugbar, no install. The editor is the debugger.

How it's built, still the same way

None of this is magic; it's composition. The props contract reuses the model→table logic from Eloquent completion and the schema plumbing already in the database layer. The Ziggy features reuse the exact route table the PHP side loads. The form contract is the props-contract machinery, pointed at forms. Every feature is two things e already had, wired together — and every one shipped compiling, unit-tested (17 new tests here), and signed & notarized by Apple in CI so the .dmg just opens.

What I hope it feels like

Less flipping between an editor, a schema tool, a TypeScript generator, and a production incident — and more the editor already understands how the two halves fit together. The truth about a VILT app is spread across PHP, TypeScript, and a database. It should be one keystroke away, not one outage away.

The space between your files is where the interesting bugs live. 0.6.4 moves in.

☕ Grab it on the releases page. If the props contract flags something it shouldn't, tell me — reconciling three languages at once is exactly the conversation worth having.