@zerotal/arch 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +67 -0
  4. package/api-surface.md +283 -0
  5. package/docs/about.md +676 -0
  6. package/docs/admin/actions.md +183 -0
  7. package/docs/admin/auth.md +89 -0
  8. package/docs/admin/dashboard.md +155 -0
  9. package/docs/admin/extending-ui.md +229 -0
  10. package/docs/admin/extending.md +205 -0
  11. package/docs/admin/forms.md +155 -0
  12. package/docs/admin/index.md +144 -0
  13. package/docs/admin/operations.md +224 -0
  14. package/docs/admin/references.md +174 -0
  15. package/docs/admin/resources.md +132 -0
  16. package/docs/admin/structure.md +158 -0
  17. package/docs/admin/tables.md +185 -0
  18. package/docs/admin/testing.md +138 -0
  19. package/docs/ai.md +373 -0
  20. package/docs/application.md +435 -0
  21. package/docs/arch.md +263 -0
  22. package/docs/assets.md +755 -0
  23. package/docs/audit.md +367 -0
  24. package/docs/authentication.md +1000 -0
  25. package/docs/authorization.md +529 -0
  26. package/docs/broadcasting/channels.md +170 -0
  27. package/docs/broadcasting/client.md +108 -0
  28. package/docs/broadcasting/events.md +190 -0
  29. package/docs/broadcasting/index.md +163 -0
  30. package/docs/broadcasting/references.md +84 -0
  31. package/docs/broadcasting/testing.md +132 -0
  32. package/docs/cache.md +387 -0
  33. package/docs/carbon.md +830 -0
  34. package/docs/changelog.md +465 -0
  35. package/docs/client/auth.md +113 -0
  36. package/docs/client/errors.md +139 -0
  37. package/docs/client/files.md +118 -0
  38. package/docs/client/index.md +128 -0
  39. package/docs/client/references.md +58 -0
  40. package/docs/client/requests.md +131 -0
  41. package/docs/client/resilience.md +141 -0
  42. package/docs/client/testing.md +146 -0
  43. package/docs/commands.md +450 -0
  44. package/docs/components.md +2427 -0
  45. package/docs/config-system.md +258 -0
  46. package/docs/container.md +596 -0
  47. package/docs/context.md +579 -0
  48. package/docs/contributing.md +164 -0
  49. package/docs/controllers.md +354 -0
  50. package/docs/conventions.md +466 -0
  51. package/docs/cookies.md +101 -0
  52. package/docs/csrf.md +206 -0
  53. package/docs/database.md +476 -0
  54. package/docs/deployment.md +434 -0
  55. package/docs/devtools.md +1013 -0
  56. package/docs/email-verification.md +320 -0
  57. package/docs/encryption.md +380 -0
  58. package/docs/errors.md +317 -0
  59. package/docs/events.md +420 -0
  60. package/docs/flow/components.md +846 -0
  61. package/docs/flow/decorators.md +458 -0
  62. package/docs/flow/events.md +420 -0
  63. package/docs/flow/forms.md +551 -0
  64. package/docs/flow/index.md +568 -0
  65. package/docs/flow/layouts.md +793 -0
  66. package/docs/flow/lifecycle.md +336 -0
  67. package/docs/flow/pagination.md +402 -0
  68. package/docs/flow/performance.md +262 -0
  69. package/docs/flow/references.md +317 -0
  70. package/docs/flow/routing.md +550 -0
  71. package/docs/flow/testing.md +447 -0
  72. package/docs/getting-started.md +286 -0
  73. package/docs/health.md +269 -0
  74. package/docs/helpers.md +423 -0
  75. package/docs/i18n.md +414 -0
  76. package/docs/index.md +65 -0
  77. package/docs/inertia/build.md +95 -0
  78. package/docs/inertia/devtools.md +197 -0
  79. package/docs/inertia/index.md +267 -0
  80. package/docs/inertia/middleware.md +81 -0
  81. package/docs/inertia/props.md +507 -0
  82. package/docs/inertia/references.md +144 -0
  83. package/docs/inertia/rendering.md +189 -0
  84. package/docs/inertia/ssr.md +102 -0
  85. package/docs/inspirations.md +257 -0
  86. package/docs/lifecycle.md +331 -0
  87. package/docs/lock.md +454 -0
  88. package/docs/logger.md +517 -0
  89. package/docs/media.md +582 -0
  90. package/docs/middleware.md +509 -0
  91. package/docs/migrations.md +529 -0
  92. package/docs/monitor.md +377 -0
  93. package/docs/notifications.md +848 -0
  94. package/docs/orm/casts.md +365 -0
  95. package/docs/orm/factories.md +231 -0
  96. package/docs/orm/index.md +663 -0
  97. package/docs/orm/lifecycle.md +421 -0
  98. package/docs/orm/queries.md +692 -0
  99. package/docs/orm/relationships.md +440 -0
  100. package/docs/orm/serialization.md +251 -0
  101. package/docs/package-development.md +536 -0
  102. package/docs/pagination.md +340 -0
  103. package/docs/password-reset.md +335 -0
  104. package/docs/providers.md +447 -0
  105. package/docs/query-builder.md +465 -0
  106. package/docs/queue.md +549 -0
  107. package/docs/rate-limiting.md +253 -0
  108. package/docs/responses.md +336 -0
  109. package/docs/roles-and-2fa.md +418 -0
  110. package/docs/routing.md +1008 -0
  111. package/docs/scaffolding.md +335 -0
  112. package/docs/scheduler.md +533 -0
  113. package/docs/seeding.md +229 -0
  114. package/docs/session.md +451 -0
  115. package/docs/social.md +524 -0
  116. package/docs/storage.md +622 -0
  117. package/docs/structure.md +230 -0
  118. package/docs/support-policy.md +114 -0
  119. package/docs/telemetry.md +505 -0
  120. package/docs/tenancy.md +608 -0
  121. package/docs/testing/browser.md +153 -0
  122. package/docs/testing/console.md +130 -0
  123. package/docs/testing/database.md +238 -0
  124. package/docs/testing/flow-browser.md +216 -0
  125. package/docs/testing/http.md +441 -0
  126. package/docs/testing/index.md +240 -0
  127. package/docs/testing/mocking.md +410 -0
  128. package/docs/upgrade.md +192 -0
  129. package/docs/validator.md +549 -0
  130. package/docs/view.md +453 -0
  131. package/package.json +60 -0
  132. package/src/bin/mcp.ts +73 -0
  133. package/src/config.ts +63 -0
  134. package/src/errors.ts +27 -0
  135. package/src/index.ts +56 -0
  136. package/src/install/ArchInstallCommand.ts +189 -0
  137. package/src/install/detect.ts +98 -0
  138. package/src/install/guidelines.ts +290 -0
  139. package/src/install/markers.ts +77 -0
  140. package/src/install/mcpConfig.ts +81 -0
  141. package/src/mcp/index.ts +27 -0
  142. package/src/mcp/jsonrpc.ts +133 -0
  143. package/src/mcp/server.ts +307 -0
  144. package/src/mcp/stdio.ts +111 -0
  145. package/src/mcp/types.ts +146 -0
  146. package/src/probe/ArchProbeCommand.ts +48 -0
  147. package/src/probe/sentinel.ts +13 -0
  148. package/src/probe/topics.ts +291 -0
  149. package/src/provider/ArchProvider.ts +101 -0
  150. package/src/tools/_probe.ts +164 -0
  151. package/src/tools/apiSurface.ts +241 -0
  152. package/src/tools/appInfo.ts +88 -0
  153. package/src/tools/baselines.ts +210 -0
  154. package/src/tools/context.ts +37 -0
  155. package/src/tools/doctor.ts +93 -0
  156. package/src/tools/index.ts +50 -0
  157. package/src/tools/logs.ts +265 -0
  158. package/src/tools/routes.ts +133 -0
  159. package/src/tools/schema.ts +119 -0
  160. package/src/tools/searchDocs.ts +345 -0
@@ -0,0 +1,507 @@
1
+ ---
2
+ title: Inertia Props
3
+ description: Pass data to a page — eager, lazy, deferred, and merged props, plus the shared props every page receives.
4
+ ---
5
+
6
+ # Data props
7
+
8
+ Zerotal implements the full **Inertia v3** server-side data-props protocol. These features let you
9
+ control _which_ props are sent, _when_ they're evaluated, and _how_ the client merges them — the
10
+ foundation for fast pages, "load more" lists, and deferred content. They work with the stock
11
+ `@inertiajs/react` / `@inertiajs/vue3` clients; no extra client setup is required.
12
+
13
+ Import the helpers directly or use the `Inertia` facade:
14
+
15
+ ```ts
16
+ // in a controller
17
+ import { inertia, optional, always, defer, merge, deepMerge } from "@zerotal/inertia";
18
+ // or
19
+ import { Inertia } from "@zerotal/inertia";
20
+ ```
21
+
22
+ > **Tip** — Not sure which wrapper to reach for? The short version: a bare value is
23
+ > always sent and always evaluated; a `() => …` function is always sent but only
24
+ > evaluated when included; `optional()` is sent only when a partial reload asks for it;
25
+ > `defer()` loads after first paint; `merge()`/`scroll()` combine new data with what
26
+ > the client already has.
27
+
28
+ ## Partial reloads
29
+
30
+ On a visit to the _same page_, the client can request a subset of props with `only` / `except`.
31
+ The server evaluates and returns just those props; the client keeps the rest. Zerotal reads the
32
+ `X-Inertia-Partial-Data` / `X-Inertia-Partial-Except` / `X-Inertia-Partial-Component` headers
33
+ automatically — you don't write any special code for the route, you just make props lazy.
34
+
35
+ ```tsx
36
+ // client
37
+ router.reload({ only: ["users"] });
38
+ ```
39
+
40
+ ### Lazy evaluation
41
+
42
+ Wrap optional data in a function so it's only evaluated when actually included:
43
+
44
+ ```ts
45
+ // in a controller
46
+ return inertia("Users/Index", {
47
+ users: () => User.all(), // evaluated every full visit, and on partial reloads that ask for it
48
+ companies: () => Company.all(),
49
+ });
50
+ ```
51
+
52
+ A function prop is **always sent** on a full visit but only **evaluated when included**, so excluding
53
+ it from a partial reload also skips the query.
54
+
55
+ ## Optional and always props
56
+
57
+ `optional(fn)` — never sent on a normal visit; only when explicitly requested via `only`. Ideal for
58
+ expensive data the page can load on demand.
59
+
60
+ ```ts
61
+ // in a controller
62
+ return inertia("Users/Index", {
63
+ users: optional(() => User.all()), // only when reloaded with only: ["users"]
64
+ });
65
+ ```
66
+
67
+ `always(value)` — always sent, even when a partial reload's `only`/`except` would exclude it. (The
68
+ shared `errors` bag uses this internally.)
69
+
70
+ | Approach | Full visit | Partial reload | Evaluated |
71
+ | ---------------------------- | ---------- | -------------- | ---------------- |
72
+ | `User.all()` | always | optionally | always |
73
+ | `() => User.all()` | always | optionally | only when needed |
74
+ | `optional(() => User.all())` | never | optionally | only when needed |
75
+ | `always(User.all())` | always | always | always |
76
+
77
+ > **Tip** — `lazy(fn)` is an alias of `optional(fn)`, kept for parity with Inertia's
78
+ > historical name. New code should prefer `optional`.
79
+
80
+ ## Deferred props
81
+
82
+ `defer(fn)` excludes a prop from the initial render and tells the client to fetch it in a follow-up
83
+ request — great for below-the-fold or slow data. Group props to control parallelism (each group is
84
+ one request).
85
+
86
+ ```ts
87
+ // in a controller
88
+ return inertia("Users/Index", {
89
+ users: () => User.all(),
90
+ permissions: defer(() => Permission.all()), // default group
91
+ teams: defer(() => Team.all(), "attributes"), // grouped together…
92
+ projects: defer(() => Project.all(), "attributes"), // …fetched in one request
93
+ });
94
+ ```
95
+
96
+ The first response carries a `deferredProps` map; the client then partial-reloads each group. On the
97
+ client, wrap the UI in `<Deferred>`:
98
+
99
+ ```tsx
100
+ // in a page component
101
+ import { Deferred } from "@inertiajs/react";
102
+
103
+ <Deferred data="permissions" fallback={<div>Loading…</div>}>
104
+ <Permissions />
105
+ </Deferred>;
106
+ ```
107
+
108
+ ### Error handling
109
+
110
+ Pass `{ rescue: true }` so a thrown error is swallowed and the key reported in `rescuedProps` (the
111
+ client renders the `<Deferred rescue>` slot) instead of failing the whole response:
112
+
113
+ ```ts
114
+ // in a controller
115
+ permissions: defer(() => Permission.all(), "default", { rescue: true }),
116
+ ```
117
+
118
+ ## Merging props
119
+
120
+ By default a reloaded prop _replaces_ the client value. `merge()` / `deepMerge()` make the client
121
+ **combine** the new data with what it already has — the basis for paginated "load more" lists. Merging
122
+ only happens on partial reloads (full visits always replace).
123
+
124
+ ```ts
125
+ // in a controller
126
+ return inertia("Feed", {
127
+ posts: merge(() => Post.paginate(15, page)), // append at root
128
+ });
129
+ ```
130
+
131
+ Chainable targeting:
132
+
133
+ ```ts
134
+ // in a controller
135
+ merge(users).append("data").matchOn("data.id"); // append to users.data, replace items matching id
136
+ merge(items).prepend(); // prepend at root
137
+ deepMerge(chat).matchOn("messages.id"); // deep-merge the whole structure
138
+ ```
139
+
140
+ These populate the page object's `mergeProps` / `prependProps` / `deepMergeProps` / `matchPropsOn`,
141
+ which the client uses to merge correctly. Use the client's `reset: ["posts"]` option to clear a prop
142
+ before merging fresh data (e.g. on a new search) — Zerotal honors the `X-Inertia-Reset` header.
143
+
144
+ ## Infinite scroll
145
+
146
+ `scroll(paginator)` is purpose-built for the client's `<InfiniteScroll>` component. It merges the
147
+ paginator's `data` array and emits a `scrollProps` entry describing the current/next/previous page,
148
+ so the client knows when (and which way) to load more:
149
+
150
+ ```ts
151
+ // app/controllers/PostController.ts
152
+ import { inertia, scroll } from "@zerotal/inertia";
153
+
154
+ async index(ctx: HttpContext) {
155
+ const page = Number(ctx.query("page", "1"));
156
+ return inertia("Posts/Index", {
157
+ posts: scroll(() => Post.paginate(15, page)), // merges posts.data, emits scrollProps
158
+ // scroll(() => Post.paginate(15, page), { pageName: "p", dataPath: "data" })
159
+ });
160
+ }
161
+ ```
162
+
163
+ This produces a page object like:
164
+
165
+ ```json
166
+ {
167
+ "mergeProps": ["posts.data"],
168
+ "scrollProps": {
169
+ "posts": { "pageName": "page", "previousPage": null, "nextPage": 2, "currentPage": 1 }
170
+ }
171
+ }
172
+ ```
173
+
174
+ `next`/`previous` page are derived from the paginator (`currentPage`/`page`, `lastPage`, or
175
+ `total`+`perPage`). When the user scrolls **up**, the client sends
176
+ `X-Inertia-Infinite-Scroll-Merge-Intent: prepend` and Zerotal prepends the new page instead of
177
+ appending — no controller change needed. On the client:
178
+
179
+ ```tsx
180
+ // in a page component
181
+ import { InfiniteScroll } from "@inertiajs/react";
182
+
183
+ <InfiniteScroll data="posts">
184
+ {posts.data.map((post) => (
185
+ <Post key={post.id} post={post} />
186
+ ))}
187
+ </InfiniteScroll>;
188
+ ```
189
+
190
+ ## Once props
191
+
192
+ Chain `.once()` onto an optional/merge/defer prop so it's resolved a single time and remembered by the
193
+ client across navigations. The client sends `X-Inertia-Except-Once-Props` with the keys it already
194
+ holds; the server skips re-resolving them.
195
+
196
+ ```ts
197
+ // in a controller
198
+ return inertia("Billing/Plans", {
199
+ plans: optional(() => Plan.all()).once(),
200
+ });
201
+ ```
202
+
203
+ ## History encryption
204
+
205
+ Encrypt the current page's browser history state (so sensitive data isn't readable from
206
+ `window.history` after logout), or clear it:
207
+
208
+ ```ts
209
+ // in a controller
210
+ import { Inertia } from "@zerotal/inertia";
211
+
212
+ Inertia.encryptHistory(); // encrypt this page's history entry
213
+ Inertia.clearHistory(); // e.g. in your logout action
214
+ ```
215
+
216
+ Set a global default in config:
217
+
218
+ ```ts
219
+ // config/inertia.ts
220
+ import { InertiaConfig } from "@zerotal/inertia";
221
+
222
+ export default InertiaConfig({ encryptHistory: true });
223
+ ```
224
+
225
+ > **Danger** — Without history encryption, sensitive props remain readable from
226
+ > `window.history` after a user logs out. Call `Inertia.clearHistory()` in your logout
227
+ > action (or set `encryptHistory: true`) for pages that render private data.
228
+
229
+ ## External & fragment redirects
230
+
231
+ `Inertia.location(url)` performs a full-page visit to an external URL — a `409` with
232
+ `X-Inertia-Location` for Inertia requests, a `302` otherwise:
233
+
234
+ ```ts
235
+ // in a controller
236
+ return Inertia.location("https://billing.stripe.com/session/abc");
237
+ ```
238
+
239
+ Redirects whose target carries a URL fragment (`/page#section`) are automatically converted to a
240
+ `409` + `X-Inertia-Redirect` on Inertia requests, so the client performs an Inertia visit that
241
+ preserves the fragment.
242
+
243
+ ## Precognition
244
+
245
+ Precognition lets a form validate against the server's real rules **without running the controller's
246
+ side effects** — perfect for live, inline validation. Register `PrecognitionMiddleware` and use a
247
+ `FormRequest`; when the client sends `Precognition: true`, `FormRequest.validate()` short-circuits
248
+ with a `204` (valid) or `422` (errors), optionally limited to the fields in `Precognition-Validate-Only`.
249
+
250
+ ```ts
251
+ // bootstrap — register PrecognitionMiddleware globally
252
+ import { PrecognitionMiddleware } from "@zerotal/inertia";
253
+
254
+ // app/controllers/PostController.ts — unchanged; validate() becomes precognition-aware automatically
255
+ async store(ctx: HttpContext) {
256
+ const data = await StorePostRequest.validate();
257
+ // ...only runs on a real (non-precognition) submit
258
+ }
259
+ ```
260
+
261
+ The middleware itself only stamps `Vary: Precognition` on precognitive responses; the validation
262
+ short-circuit happens inside `FormRequest.validate()`. On the client, use the `@inertiajs/react`
263
+ precognition `useForm().validate(...)` helpers as usual.
264
+
265
+ ## Page object reference
266
+
267
+ Each feature contributes fields to the JSON [page object](https://inertiajs.com/docs/v3/core-concepts/the-protocol#the-page-object).
268
+ Zerotal emits these automatically; you never build them by hand. Fields are omitted when empty.
269
+
270
+ | Field | Set by |
271
+ | ------------------------------------------------ | --------------------------------------------------- |
272
+ | `component`, `props`, `url`, `version` | always (core) |
273
+ | `deferredProps` | `defer()` — `{ group: [keys] }` on first load |
274
+ | `rescuedProps` | a rescued `defer()` prop that threw |
275
+ | `mergeProps` / `prependProps` / `deepMergeProps` | `merge()` / `.prepend()` / `deepMerge()` |
276
+ | `matchPropsOn` | `.matchOn(...)` |
277
+ | `scrollProps` | `scroll()` — infinite-scroll pagination config |
278
+ | `onceProps` | `.once()` |
279
+ | `encryptHistory` / `clearHistory` | `Inertia.encryptHistory()` / `clearHistory()` |
280
+ | `sharedProps` | keys registered via `Inertia.share()` (+ built-ins) |
281
+
282
+ ### Request headers Zerotal reads
283
+
284
+ `X-Inertia`, `X-Inertia-Version`, `X-Inertia-Partial-Data`, `X-Inertia-Partial-Except`,
285
+ `X-Inertia-Partial-Component`, `X-Inertia-Reset`, `X-Inertia-Except-Once-Props`,
286
+ `X-Inertia-Error-Bag`, and `Precognition` / `Precognition-Validate-Only`.
287
+
288
+ ## Shared props
289
+
290
+ Shared props are merged into **every** Inertia page automatically, so common data
291
+ like the authenticated user and flash messages are always available to your
292
+ components without each controller passing them explicitly.
293
+
294
+ ```ts
295
+ // in a controller
296
+ import { sharedProps } from "@zerotal/inertia";
297
+ ```
298
+
299
+ `inertia()` calls `sharedProps()` internally and spreads the result **under** your
300
+ controller's props (`{ ...sharedProps(), ...props }`), so a prop you pass with the
301
+ same key wins.
302
+
303
+ ### What's provided
304
+
305
+ ```ts
306
+ // the shared bag sharedProps() returns
307
+ {
308
+ auth: {
309
+ user: ctx.user ?? null, // reduced to plain scalars — never a live ORM model
310
+ },
311
+ flash: {
312
+ success: session.get("success") ?? null,
313
+ error: session.get("error") ?? null,
314
+ },
315
+ errors: always(session.get("errors") ?? {}), // from a validation redirect
316
+ old: session.get("old") ?? {}, // previous form input
317
+ }
318
+ ```
319
+
320
+ `errors` is wrapped in [`always()`](#optional-and-always-props) so it survives
321
+ [partial reloads](#partial-reloads) — the Inertia client always expects an
322
+ `errors` bag. When the request carries an `X-Inertia-Error-Bag` header, errors are namespaced under
323
+ that bag. The other shared props (`auth`/`flash`/`old`) are ordinary props, so a partial reload's
324
+ `only`/`except` filter applies to them, matching Inertia's semantics.
325
+
326
+ Every page component can read these without the controller passing them:
327
+
328
+ ```tsx
329
+ // resources/js/pages/Page.tsx
330
+ import { usePage } from "@inertiajs/react";
331
+
332
+ export default function Page() {
333
+ const { auth, flash, errors } = usePage<{
334
+ auth: { user: { name: string } | null };
335
+ flash: { success: string | null; error: string | null };
336
+ errors: Record<string, string>;
337
+ }>().props;
338
+
339
+ return (
340
+ <>
341
+ {flash.success && <div className="toast">{flash.success}</div>}
342
+ {auth.user ? <span>{auth.user.name}</span> : <a href="/login">Sign in</a>}
343
+ </>
344
+ );
345
+ }
346
+ ```
347
+
348
+ #### Why auth.user is a plain object
349
+
350
+ `sharedProps()` serializes the authenticated user to **scalar fields only** — it
351
+ skips methods and array-valued properties. This is deliberate: a live model with an
352
+ unloaded `@hasMany` relation throws when `JSON.stringify` touches the relation getter.
353
+ The plain object is always safe to send. If a page needs related data, load it
354
+ explicitly in the controller and pass it as a normal prop.
355
+
356
+ > **Warning** — Don't pass a live ORM model as a prop expecting all its relations.
357
+ > `JSON.stringify` triggers relation getters, which throw if the relation wasn't
358
+ > eager-loaded. Eager-load (`.with(...)`) or map to a plain shape first.
359
+
360
+ ### Adding custom shared props
361
+
362
+ #### Register with Inertia.share
363
+
364
+ Register props once — typically in a provider's boot or in middleware — and they're merged into
365
+ every page. Values may be plain values, factory functions (evaluated lazily per request), or any
366
+ [prop wrapper](#data-props):
367
+
368
+ ```ts
369
+ // in a provider's boot or middleware
370
+ import { Inertia } from "@zerotal/inertia";
371
+
372
+ // Single key, or a map:
373
+ Inertia.share("appName", "Acme");
374
+ Inertia.share({
375
+ appName: "Acme",
376
+ year: () => new Date().getFullYear(), // evaluated per request
377
+ flags: Inertia.optional(() => FeatureFlag.all()), // only on partial reloads
378
+ });
379
+ ```
380
+
381
+ Shared props are subject to partial-reload filtering just like page props (except `errors`, which is
382
+ `always()`), and they appear in the page object's `sharedProps` list so the client can carry them
383
+ over during instant visits.
384
+
385
+ #### Merge in the controller
386
+
387
+ ```ts
388
+ // in a controller
389
+ return inertia("Dashboard", {
390
+ notifications: await Notification.query().where("user_id", user.id).unread().get(),
391
+ // auth, flash, errors, old are merged automatically — no need to repeat them
392
+ });
393
+ ```
394
+
395
+ #### Share from middleware via the context
396
+
397
+ When several pages need the same extra data, set it on the context in a middleware
398
+ and read it back in the controller — keeping the controller body clean:
399
+
400
+ ```ts
401
+ // in a middleware:
402
+ ctx.setInternal("unreadCount", await Notification.unreadCount(ctx.user!.id));
403
+
404
+ // in the controller:
405
+ return inertia("Layout", {
406
+ unreadCount: ctx.getInternal<number>("unreadCount"),
407
+ });
408
+ ```
409
+
410
+ This pattern pairs well with a persistent [layout](/docs/inertia/build#generating-a-page) that
411
+ displays the value on every page.
412
+
413
+ ## Typed props
414
+
415
+ The props a controller passes are checked against the props the page component
416
+ declares:
417
+
418
+ ```tsx
419
+ // resources/js/pages/Posts/Show.tsx
420
+ interface Props {
421
+ post: Post;
422
+ related: Post[];
423
+ stats?: { views: number }; // optional — it arrives after first paint
424
+ }
425
+ export default function Show({ post, related, stats }: Props) { … }
426
+ ```
427
+
428
+ ```ts
429
+ // in a controller
430
+ return Inertia.render("Posts/Show", {
431
+ post,
432
+ related: [],
433
+ stats: defer(() => computeStats()),
434
+ });
435
+
436
+ Inertia.render("Posts/Shwo", { … }); // ✗ not a page
437
+ Inertia.render("Posts/Show", { post }); // ✗ Property 'related' is missing
438
+ Inertia.render("Posts/Show", { post, relatd: [] }); // ✗ Did you mean 'related'?
439
+ ```
440
+
441
+ Nothing is annotated to make this work. The component already declares its props,
442
+ and `resources/js/pages.generated.ts` already holds an `import()` thunk per page —
443
+ and an `import()` thunk carries the module's full type. The registry is written
444
+ with `satisfies`, so those types survive, and one type-only line in that same file
445
+ hands them to the server. Rebuild it with `bun zt inertia:build` (or just run
446
+ `zt dev`, which rebuilds it on every change).
447
+
448
+ The check runs in the direction that costs nothing: the **component** declares
449
+ the shape and the **controller** is checked against it.
450
+
451
+ ### Wrappers are unwrapped
452
+
453
+ The two sides genuinely differ — the controller passes `merge(() => posts)` and
454
+ the component receives `Post[]` — so each prop accepts its value, a factory for
455
+ it, or a wrapper carrying it, and the wrapper's payload is checked against the
456
+ prop it fills:
457
+
458
+ ```ts
459
+ Inertia.render("Posts/Show", { post, related: merge(() => [1, 2]) });
460
+ // ✗ number[] is not Post[]
461
+ ```
462
+
463
+ > **Warning** — `optional()` and `defer()` are only accepted where the component
464
+ > declares the prop as optional. They are _absent on first paint_ by definition,
465
+ > so a component that types such a prop as required is wrong about its own
466
+ > contract. Types that accepted it anyway would launder that bug into something
467
+ > the compiler had signed off on. Add the `?` — and handle the undefined.
468
+
469
+ ### Shared props are never required
470
+
471
+ `auth`, `flash`, `errors` and `old` are merged into every page, so a controller
472
+ never has to pass them — even when the page component declares them. Props you
473
+ register yourself with `Inertia.share()` are a runtime call that nothing can
474
+ generate, so declare them once:
475
+
476
+ ```ts
477
+ // resources/js/types.ts (next to the interface your pages read with usePage)
478
+ declare module "@zerotal/inertia" {
479
+ interface SharedProps {
480
+ appName: string;
481
+ flags: Record<string, boolean>;
482
+ }
483
+ }
484
+ ```
485
+
486
+ They then become optional-but-accepted in every `Inertia.render` call: a page may
487
+ still override one, and no page is forced to pass it.
488
+
489
+ ### What is not checked
490
+
491
+ - **A page name only known at runtime** — an error page chosen by status code, a
492
+ component from config — has nothing to check against. Use
493
+ `Inertia.render.dynamic(name, props)`, which takes any name and any props.
494
+ - **Vue pages.** A `.vue` SFC resolves through a `declare module '*.vue'` shim
495
+ that types the default export as `DefineComponent<{}, {}, any>`, so its props
496
+ are not visible to TypeScript unless `vue-tsc` is in your typecheck path. Those
497
+ pages fall back to accepting any props rather than failing on a shape nobody
498
+ can see. The page **name** is still checked; only its props are not.
499
+ - **A component wrapped in `React.memo()`** (or anything else that returns an
500
+ object rather than a function) falls back the same way.
501
+ - **Before you rebuild the registry**, every name and every prop bag compiles, as
502
+ it always did.
503
+
504
+ ## Next steps
505
+
506
+ - [Inertia overview](/docs/inertia) — the guide's front page and the rest of the sections.
507
+ - [Reference](/docs/inertia/references) — the full API surface in one table.
@@ -0,0 +1,144 @@
1
+ ---
2
+ title: Inertia Reference
3
+ description: Every helper, prop type, and config key in one table.
4
+ ---
5
+
6
+ # References
7
+
8
+ A consolidated cheat-sheet for `@zerotal/inertia`. Each entry links to the section above where it's explained in full.
9
+
10
+ ## Commands
11
+
12
+ | Command | Purpose |
13
+ | -------------------------------------------------- | --------------------------------------------------------------- |
14
+ | `bun zt make:page <Name>` | Scaffold a page component under your pages directory. |
15
+ | `bun zt make:page <Name> --layout <Layout>` | Scaffold a page wrapped in a persistent layout. |
16
+ | `bun zt make:page <Name> --framework <vue\|react>` | Force the frontend framework instead of auto-detecting. |
17
+ | `bun zt inertia:build` | Bundle the frontend — development build (external source maps). |
18
+ | `bun zt inertia:build -p` | Production build (minified, no source maps). |
19
+
20
+ ## Server exports
21
+
22
+ | Export | Purpose |
23
+ | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
24
+ | `inertia(component, props?)` | Render an Inertia page from a controller action. See [the inertia helper](/docs/inertia/rendering#the-inertia-helper). |
25
+ | `inertia.dynamic(component, props?)` | Render a page whose name is only known at runtime — no compile-time checking. See [what is not checked](/docs/inertia/props#what-is-not-checked). |
26
+ | `inertiaStream(component, props?)` | Streaming SSR alternative to `inertia()`. See [Streaming SSR](/docs/inertia/ssr#streaming-ssr). |
27
+ | `inertiaStream.dynamic(component, props?)` | The streaming counterpart of `inertia.dynamic()`. |
28
+ | `optional(fn)` / `lazy(fn)` | Prop sent only on a partial reload that requests it. See [optional and always props](/docs/inertia/props#optional-and-always-props). |
29
+ | `always(value)` | Prop always sent, even when a partial reload would exclude it. |
30
+ | `defer(fn, group?, opts?)` | Prop fetched in a follow-up request after first paint. See [deferred props](/docs/inertia/props#deferred-props). |
31
+ | `merge(value)` / `deepMerge(value)` | Combine reloaded data with existing client data. See [merging props](/docs/inertia/props#merging-props). |
32
+ | `scroll(paginator, opts?)` | Infinite-scroll pagination wired to `<InfiniteScroll>`. See [infinite scroll](/docs/inertia/props#infinite-scroll). |
33
+ | `sharedProps()` | The auto-merged `auth` / `flash` / `errors` / `old` bag. See [Shared Props](/docs/inertia/props). |
34
+ | `share(key, value)` / `share(map)` | Register custom shared props. See [adding custom shared props](/docs/inertia/props#adding-custom-shared-props). |
35
+ | `setAssetVersion(v)` / `assetVersion()` | Set / read the current asset version. See [asset versioning](/docs/inertia/middleware#asset-versioning). |
36
+ | `generatePageRegistry(cwd?)` | Regenerate the page registry module. See [page registry](/docs/inertia/build#page-registry). |
37
+ | `inertiaRoute(path, component, props?, middleware?)` | The function behind the `Router.inertia()` macro. See [controller-less routes](/docs/inertia/rendering#controller-less-routes). |
38
+ | `InertiaProvider` | Wires the middleware, template, asset version, and optional SSR endpoint. See [Register the provider](/docs/inertia#register-the-provider). |
39
+ | `InertiaMiddleware` | Protocol mechanics; auto-registered by the provider. See [Middleware & versioning](/docs/inertia/middleware). |
40
+ | `PrecognitionMiddleware` | Enables precognition validation. See [precognition](/docs/inertia/props#precognition). |
41
+ | `InertiaConfig(options?)` | Build a typed `config/inertia.ts` object with defaults. See [Configuration](/docs/inertia#configuration). |
42
+ | `Inertia` | Unified facade — see below. |
43
+
44
+ ## The Inertia facade
45
+
46
+ | Method | Equivalent / purpose |
47
+ | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
48
+ | `Inertia.render(component, props?)` | Same as `inertia()`. |
49
+ | `Inertia.stream(component, props?)` | Same as `inertiaStream()`. |
50
+ | `Inertia.optional(fn)` / `Inertia.lazy(fn)` | Same as `optional()`. |
51
+ | `Inertia.always(value)` | Same as `always()`. |
52
+ | `Inertia.defer(fn, group?, opts?)` | Same as `defer()`. |
53
+ | `Inertia.merge(value)` / `Inertia.deepMerge(value)` | Same as `merge()` / `deepMerge()`. |
54
+ | `Inertia.scroll(paginator, opts?)` | Same as `scroll()`. |
55
+ | `Inertia.share(key, value)` / `Inertia.share(map)` | Register custom shared props. See [adding custom shared props](/docs/inertia/props#adding-custom-shared-props). |
56
+ | `Inertia.location(url)` | External / full-page redirect (`409` + `X-Inertia-Location`). See [external & fragment redirects](/docs/inertia/props#external-fragment-redirects). |
57
+ | `Inertia.encryptHistory()` | Encrypt this page's history entry. See [history encryption](/docs/inertia/props#history-encryption). |
58
+ | `Inertia.clearHistory()` | Clear encrypted history (e.g. on logout). |
59
+
60
+ ## Prop helpers
61
+
62
+ | Helper / chain | Behaviour |
63
+ | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
64
+ | `optional(fn)` | Never sent on a normal visit; only on a partial reload that asks for it. |
65
+ | `lazy(fn)` | Alias of `optional(fn)`. |
66
+ | `always(value)` | Always sent, even past a partial reload's include / exclude filter. |
67
+ | `defer(fn, group?, { rescue? })` | Excluded from the first render; fetched in a grouped follow-up request. |
68
+ | `merge(value)` / `deepMerge(value)` | Append / deep-merge reloaded data instead of replacing it. |
69
+ | `.append(path)` | Append merged data at `path`. |
70
+ | `.prepend(path?)` | Prepend merged data at the root (no arg) or at `path`. |
71
+ | `.matchOn(path)` | Replace items matching the key at `path`. |
72
+ | `.once(expiresAt?)` | Resolve a single time; the client remembers it across navigations. See [once props](/docs/inertia/props#once-props). |
73
+
74
+ Each helper returns a wrapper class — `OptionalProp`, `AlwaysProp`, `DeferProp`,
75
+ `MergeProp`, `InfiniteScrollProp`, all extending `InertiaProp`. You build them
76
+ through the helpers above rather than constructing them; the names matter only for
77
+ annotating a variable or narrowing a union, and `InertiaProp` is the type to accept
78
+ when a function takes "any wrapped prop".
79
+
80
+ ## Errors
81
+
82
+ | Error | Thrown when |
83
+ | ------------------------------- | ---------------------------------------------------------------------------------- |
84
+ | `InertiaError` | Base class for the rest — catch this to handle any Inertia failure. |
85
+ | `InvalidComponentError` | A page component name is empty or not a string. |
86
+ | `InertiaTemplateNotLoadedError` | The root template was never loaded, so there is no HTML shell to render a page in. |
87
+
88
+ ## Server-side rendering
89
+
90
+ `SsrHandler` is the class behind `InertiaConfig({ ssr: true })` — it owns the
91
+ `POST /__ssr` route the config registers. Configure SSR through the config flag; the
92
+ class is exported for tests that drive the handler directly. See
93
+ [SSR](/docs/inertia/ssr).
94
+
95
+ ## Configuration options
96
+
97
+ Set in `config/inertia.ts` via `InertiaConfig({ … })` — every field is optional.
98
+
99
+ | Option | Default | Purpose |
100
+ | ---------------- | ------------------------ | ---------------------------------------------------------------------------------------------------- |
101
+ | `htmlTemplate` | `"./resources/app.html"` | Path to the root HTML template (must contain `<!-- @inertia -->`); falls back to a built-in default. |
102
+ | `version` | `"1"` | Asset version string embedded in every page object; bump on each deploy. |
103
+ | `assetsUrl` | `"/"` | Public URL prefix for built assets. |
104
+ | `pagesDir` | `"resources/js/pages"` | Directory where Inertia page components live. |
105
+ | `ssr` | `false` | Register `POST /__ssr` for endpoint SSR. |
106
+ | `encryptHistory` | `false` | Encrypt history state globally. |
107
+
108
+ ## Protocol headers
109
+
110
+ | Header | Direction | Used for |
111
+ | ----------------------------------------------------- | ------------------ | ------------------------------------------------------------ |
112
+ | `X-Inertia` | request / response | Marks an Inertia XHR visit; echoed on the response. |
113
+ | `X-Inertia-Version` | request | Client's asset version — a mismatch triggers a `409` reload. |
114
+ | `X-Inertia-Partial-Data` / `X-Inertia-Partial-Except` | request | Partial-reload prop include / exclude lists. |
115
+ | `X-Inertia-Partial-Component` | request | The component a partial reload targets. |
116
+ | `X-Inertia-Reset` | request | Props to clear before merging fresh data. |
117
+ | `X-Inertia-Except-Once-Props` | request | Once-props the client already holds. |
118
+ | `X-Inertia-Error-Bag` | request | Namespaces validation errors under a named bag. |
119
+ | `X-Inertia-Infinite-Scroll-Merge-Intent` | request | `prepend` vs append intent when scrolling up. |
120
+ | `Precognition` / `Precognition-Validate-Only` | request | Precognition validation and field scoping. |
121
+ | `Vary: X-Inertia` | response | Keeps cached HTML and JSON variants separate. |
122
+ | `X-Inertia-Location` | response | `409` full-page reload / external redirect target. |
123
+ | `X-Inertia-Redirect` | response | `409` redirect that preserves a URL fragment. |
124
+
125
+ ## Page object fields
126
+
127
+ See [page object reference](/docs/inertia/props#page-object-reference) for the full table of fields — `component`, `props`, `url`, `version`, `deferredProps`, `mergeProps`, `scrollProps`, `onceProps`, and the rest — and which API sets each.
128
+
129
+ ## Types
130
+
131
+ Everything here is type-only, and exists so `Inertia.render` can be checked against
132
+ the page it names. See [Typed props](/docs/inertia/props#typed-props).
133
+
134
+ | Type | Purpose |
135
+ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
136
+ | `InertiaPageRegistry` | Augmentation target the generated `pages.generated.ts` fills with `{ pages: typeof pages }`. Empty until the registry is built. |
137
+ | `SharedProps` | Declare your own `Inertia.share()` keys here; they become optional-but-accepted in every render call. |
138
+ | `PageName` | Union of the generated page names (`never` before the registry exists). |
139
+ | `PageTarget` | What the helpers accept as a name: `PageName` once generated, `string` before that. |
140
+ | `PropsOf<N>` | The props page `N`'s component declares. |
141
+ | `PropInput<T>` | What may be passed for a prop typed `T` — the value, a factory, or a wrapper carrying it. |
142
+ | `RenderProps<N>` | The full props bag `Inertia.render(N, …)` accepts, shared props subtracted. |
143
+ | `RenderArgs<N>` | `render()`'s argument tuple; the props bag is optional only when the page requires nothing. |
144
+ | `PageRenderer` | The shape of `inertia` / `inertiaStream`: a checked call signature plus `.dynamic`. |