|
1 | 1 | # vite-plugin-ferry |
2 | 2 |
|
3 | | -> Type-safe Inertia apps end to end — generates TypeScript for routes, enums, resources, and page props straight from your Laravel backend. |
| 3 | +> Type-safe Inertia apps end to end — generates TypeScript for routes, enums, resources, page props, and form types straight from your Laravel backend. |
4 | 4 |
|
5 | 5 | [](https://www.npmjs.com/package/vite-plugin-ferry) |
6 | 6 | [](https://www.npmjs.com/package/vite-plugin-ferry) |
|
12 | 12 | - 🏷️ **Enums** — PHP enums become real JS classes with `is`/`from`/`fromOrFail`/`values`/`keys`/`cases`/`options`, narrowed to their literal value union |
13 | 13 | - 📦 **Resources** — precise types for your `JsonResource` classes from static shape analysis plus real column/cast metadata, degrading gracefully instead of breaking your build |
14 | 14 | - 🧩 **Page props** — the props an Inertia page receives, typed through `usePage<T>()` and Inertia's own `sharedPageProps` augmentation |
| 15 | +- 📝 **Form types** — the data shape of your `FormRequest` classes, typed through `useForm<T>()` with `form.errors` keys derived for free |
15 | 16 | - ⚛️ **React, Vue & Svelte** — `route()` and `route.isCurrent()` resolve through ferry on every frontend, in dev and production (the `.url` codemod sugar is React/plain-TS only) |
16 | 17 |
|
17 | 18 | Nothing is written into your project tree. Runtime code is delivered as Vite virtual modules, types as one generated ambient `.d.ts`, and everything regenerates on every run. |
@@ -234,6 +235,48 @@ public function share(Request $request): array |
234 | 235 |
|
235 | 236 | When `share()` calls `parent::share()`, ferry follows it into an app-local base middleware and merges that parent's shared props in too — the child wins on any key collision. A vendor or otherwise unlocatable parent (such as Inertia's base `Middleware`) is skipped silently. |
236 | 237 |
|
| 238 | +### Form types — `@ferry/forms` |
| 239 | + |
| 240 | +Ferry reads every `FormRequest`'s `rules()` and generates a data-shape type per request, named by the class's verbatim short name and served from `@ferry/forms`. Pass it to `useForm<T>()` to type both the form data and — for free — the `form.errors` keys, which Inertia derives from the same shape via its own `FormDataKeys<T>`, including nested (`profile.bio`) and array (`items.0.id`) paths. |
| 241 | + |
| 242 | +```php |
| 243 | +// app/Http/Requests/StoreUserRequest.php |
| 244 | +public function rules(): array |
| 245 | +{ |
| 246 | + return [ |
| 247 | + 'name' => 'required|string', |
| 248 | + 'age' => 'nullable|integer', |
| 249 | + 'role' => 'required|in:admin,editor,viewer', |
| 250 | + 'profile.bio' => ['nullable', 'string'], |
| 251 | + 'items.*.id' => ['required', 'integer'], |
| 252 | + ]; |
| 253 | +} |
| 254 | +``` |
| 255 | + |
| 256 | +```ts |
| 257 | +declare module '@ferry/forms' { |
| 258 | + export type StoreUserRequest = { |
| 259 | + name: string; |
| 260 | + age: number | null; |
| 261 | + role: 'admin' | 'editor' | 'viewer'; |
| 262 | + profile: { bio: string | null }; |
| 263 | + items: { id: number }[]; |
| 264 | + }; |
| 265 | +} |
| 266 | +``` |
| 267 | + |
| 268 | +```tsx |
| 269 | +// resources/js/Pages/Users/Create.tsx |
| 270 | +import type { StoreUserRequest } from '@ferry/forms'; |
| 271 | + |
| 272 | +const form = useForm<StoreUserRequest>({ name: '', age: null, role: 'admin', profile: { bio: null }, items: [] }); |
| 273 | +form.data.role; // 'admin' | 'editor' | 'viewer' |
| 274 | +form.errors['profile.bio']; // typed error key, derived from the shape |
| 275 | +form.errors['items.0.id']; // wildcard array paths resolve too |
| 276 | +``` |
| 277 | + |
| 278 | +Rule tokens map to leaf types — `string`/`email`/`url`/`uuid`/`date` → `string`, `integer`/`numeric`/`decimal` → `number`, `boolean` → `boolean`, `in:a,b,c` → a string-literal union, and `array` → `any[]` unless nested keys describe its shape. Dotted keys nest (`profile.bio` → `profile: { bio: ... }`) and a `*` segment becomes an array (`items.*.id` → `items: { id: ... }[]`). `nullable` unions `| null` onto the value; `sometimes` makes the key optional; every other field is present, since a form initializes all of them. A field whose only rule ferry can't map to a type — a `Rule::` object, a closure, or a rule with no type signal — degrades to `any` (or `unknown` under [`strict`](#configuration)) with a warning. A form with no matching `FormRequest` generates no type, and its `useForm()` call simply omits the generic as before. |
| 279 | + |
237 | 280 | ## Configuration |
238 | 281 |
|
239 | 282 | ```ts |
|
0 commit comments