@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,197 @@
1
+ ---
2
+ title: Inertia DevTools
3
+ description: Record every Inertia request — component, props, timing, and headers — and read it in the Inertia DevTools browser extension.
4
+ ---
5
+
6
+ # Inertia DevTools
7
+
8
+ The [Inertia DevTools](https://inertiajs.com/docs/v3/advanced/devtools) browser
9
+ extension shows a timeline of every Inertia request: which component rendered,
10
+ which props it received, which of those were deferred or shared, and how long the
11
+ server took. Zerotal implements the server half of that — the recorder and the
12
+ read API the extension talks to.
13
+
14
+ It is on by default in development and off everywhere else, so there is normally
15
+ nothing to configure.
16
+
17
+ ## What you get
18
+
19
+ Install the extension, run `bun zt dev`, and open the Inertia panel. Each request
20
+ becomes one entry carrying:
21
+
22
+ - The **component** that rendered and the **route** that matched, by name.
23
+ - Every **prop**, tagged with the wrapper that produced it — `defer` (and its
24
+ group), `optional`, `always`, `merge` (and its direction), `once`, `scroll` —
25
+ plus which props came from `share()`.
26
+ - The **resolved values**, with sensitive keys removed.
27
+ - **Request and response headers**, with sensitive ones removed.
28
+ - **Status, method, URL, and server time**.
29
+
30
+ Follow-up requests are grouped with the navigation that caused them, so a page
31
+ whose deferred props arrive in three later requests reads as one batch rather
32
+ than four unrelated rows.
33
+
34
+ ### The same data, without the extension
35
+
36
+ The recorder also feeds Zerotal's own [DevTools](/docs/devtools) panel, on an
37
+ **Inertia** tab, whenever that package is installed. Nothing to configure and no
38
+ extension to add — the entry is recorded against the same request as everything
39
+ else, so one row shows the component and its prop tree _and_ the SQL that
40
+ produced them. A visit and the deferred loads it triggers fold into one entry on
41
+ that panel's All tab, keyed on the same batch the extension groups by.
42
+
43
+ This is a fan-out, not a replacement: the read API below is a published contract
44
+ and keeps serving the extension either way. Neither package depends on the other
45
+ — the panel is found through the container, and its absence is the ordinary case.
46
+
47
+ ### The client half needs Inertia 3
48
+
49
+ Two independent halves feed the panel. The **server recorder** is this package,
50
+ and it works on any supported Inertia version. The **client hooks** — visit
51
+ options, prefetch-cache entries, and the grouping that tells a poll apart from a
52
+ navigation — live in the Inertia adapter, and only in **version 3 or later**.
53
+
54
+ On an older adapter the panel still fills with requests, but reports that the app
55
+ is not running in dev mode and suggests starting a Vite dev server. That advice
56
+ does not apply here — there is no Vite in a Zerotal app — and the fix is the
57
+ adapter version:
58
+
59
+ ```bash
60
+ # in your project root
61
+ bun add @inertiajs/react@^3 # or @inertiajs/vue3@^3
62
+ ```
63
+
64
+ Apps scaffolded from Zerotal 1.6.1 onwards already pin it. The React adapter
65
+ requires React 19, which the template has always pinned.
66
+
67
+ Nothing else is needed. The adapter's `dev` option defaults to `import.meta.env.DEV`,
68
+ a Vite convention that Bun's bundler does not define on its own — so Zerotal
69
+ defines it for every Inertia build, development and production alike. You do not
70
+ set `dev` yourself, and there is no Vite server to run.
71
+
72
+ ## Turning it on and off
73
+
74
+ The recorder follows the same gate as the stack-trace error page: on when this
75
+ process is a development process, off otherwise. A production deploy records
76
+ nothing and registers no endpoints — there is nothing there to probe.
77
+
78
+ To override, set the environment variable:
79
+
80
+ ```bash
81
+ INERTIA_DEVTOOLS_ENABLED=false # off, even in dev
82
+ INERTIA_DEVTOOLS_ENABLED=true # on — see the warning below
83
+ ```
84
+
85
+ Or configure it:
86
+
87
+ ```typescript
88
+ // config/inertia.ts
89
+ import { InertiaConfig } from "zerotal/inertia";
90
+
91
+ export default InertiaConfig({
92
+ devtools: {
93
+ enabled: null, // null = follow the dev-surface gate (default)
94
+ maxEntries: 200,
95
+ },
96
+ });
97
+ ```
98
+
99
+ > **Running it outside development.** Entries contain request headers and
100
+ > resolved props for real users' requests. If you enable the recorder on a shared
101
+ > environment, set a `gate` — without one the read API refuses every request
102
+ > rather than defaulting to open.
103
+
104
+ ## Configuration
105
+
106
+ All keys live under `devtools` in `config/inertia.ts`, typed as
107
+ `InertiaDevtoolsConfig`.
108
+
109
+ | Key | Default | What it does |
110
+ | --------------- | ------- | ------------------------------------------------------------------------------ |
111
+ | `enabled` | `null` | `null` follows the dev-surface gate; `true`/`false` decides explicitly. |
112
+ | `maxEntries` | `200` | How many entries to keep before the oldest is dropped. |
113
+ | `redact` | `[]` | Extra prop/body key patterns to withhold, on top of the built-in list. |
114
+ | `redactHeaders` | `[]` | Extra header names to withhold, on top of the built-in list. |
115
+ | `except` | `[]` | Path prefixes never recorded. |
116
+ | `gate` | `null` | `(request) => boolean` authorising the read API outside a development process. |
117
+
118
+ ### Redaction
119
+
120
+ Values are redacted **before** an entry is stored, so a withheld value is never
121
+ written down in the first place.
122
+
123
+ Redacted out of the box: any key containing `password`, `secret`, `token`,
124
+ `authorization`, `api_key`, `apikey`, `credit_card`, `card_number`, `cvv`, `ssn`,
125
+ or `private_key`, and the `authorization`, `cookie`, `set-cookie`,
126
+ `proxy-authorization`, `x-api-key`, `x-csrf-token`, and `x-xsrf-token` headers.
127
+ Matching is a case-insensitive substring, so `password` also covers
128
+ `password_confirmation` and `currentPassword`.
129
+
130
+ Add your own:
131
+
132
+ ```typescript
133
+ // config/inertia.ts
134
+ export default InertiaConfig({
135
+ devtools: {
136
+ redact: ["national_id", "account_number"],
137
+ redactHeaders: ["x-internal-signature"],
138
+ },
139
+ });
140
+ ```
141
+
142
+ Uploaded files are summarised rather than inlined, and a prop graph containing a
143
+ cycle is recorded with `[Circular]` in place of the loop instead of failing the
144
+ request.
145
+
146
+ ### Keeping noise out
147
+
148
+ The read API never records itself. Add anything else that would bury the
149
+ timeline:
150
+
151
+ ```typescript
152
+ // config/inertia.ts
153
+ export default InertiaConfig({
154
+ devtools: { except: ["/health", "/metrics"] },
155
+ });
156
+ ```
157
+
158
+ ### Gating a shared environment
159
+
160
+ ```typescript
161
+ // config/inertia.ts
162
+ export default InertiaConfig({
163
+ devtools: {
164
+ enabled: true,
165
+ gate: (request) => request.headers.get("X-Debug-Key") === Bun.env["DEBUG_KEY"],
166
+ },
167
+ });
168
+ ```
169
+
170
+ The gate is never consulted on a development machine — a gate that can lock you
171
+ out of your own laptop is a gate that gets switched off.
172
+
173
+ ## How it works
174
+
175
+ `InertiaProvider` registers `InertiaDevtoolsMiddleware` and the read API when the
176
+ recorder is enabled, so an app adds nothing to its middleware stack.
177
+
178
+ Each response carries the id of the entry it produced, and the first full-page
179
+ load also embeds it in a script tag so the extension can find the entry before
180
+ any XHR happens. The extension then reads the entry from
181
+ `/_inertia/devtools/entries` (`DEVTOOLS_API_PREFIX`), which serves
182
+ `DevtoolsEntry` objects newest-first and accepts `component`, `type`, `exclude`,
183
+ `offset`, and `limit` filters.
184
+
185
+ Entries are kept in memory in the process that recorded them, capped at
186
+ `maxEntries`. They do not survive a restart — which under `bun zt dev` happens on
187
+ every save anyway, and which keeps recording off the disk and out of the request
188
+ path.
189
+
190
+ `devtoolsEnabled()` reports whether the recorder is currently active, if you need
191
+ to branch on it yourself.
192
+
193
+ ## Next steps
194
+
195
+ - [Props](/docs/inertia/props) — the wrappers the panel reports on.
196
+ - [Middleware & Versioning](/docs/inertia/middleware) — the rest of the Inertia middleware stack.
197
+ - [DevTools](/docs/devtools) — Zerotal's own in-browser panel for SQL, logs, mail, and jobs.
@@ -0,0 +1,267 @@
1
+ ---
2
+ title: Inertia
3
+ description: Build a single-page app with server-side routing and controllers — no separate API layer or client-side router.
4
+ ---
5
+
6
+ # Inertia
7
+
8
+ [Inertia.js](https://inertiajs.com) lets you build a single-page app using
9
+ server-side routing and controllers — no separate API layer, no client-side router.
10
+ Your controllers return Inertia **page responses**; Inertia renders the matching
11
+ React (or Vue) component client-side on navigation, and returns a full HTML document
12
+ on the first load.
13
+
14
+ Zerotal's `@zerotal/inertia` package is a native, Bun-powered Inertia server adapter with full
15
+ **Inertia v3** protocol support: controller-less page routes, automatic shared props, asset
16
+ versioning, the complete data-props layer (partial reloads, `optional`/`defer`/`merge`/`once` props,
17
+ history encryption, precognition), a `make:page` generator, a `Bun.build`-based bundler, and optional
18
+ streaming SSR — all wired by a single provider. The stock `@inertiajs/react` / `@inertiajs/vue3`
19
+ clients work against it unchanged.
20
+
21
+ ## How it works
22
+
23
+ 1. **First load** — a `GET` request hits a controller, which calls `inertia()`. The
24
+ server renders the full HTML document with the page object (component name +
25
+ props) embedded as JSON.
26
+ 2. **Client boot** — the Inertia client reads that page object and mounts the named
27
+ component.
28
+ 3. **Navigation** — subsequent links/visits send an `X-Inertia: true` request. The
29
+ same controller runs, but `inertia()` returns **only the JSON page object** — the
30
+ client swaps the page component without a full reload.
31
+
32
+ The server stays the source of truth for routing and data; the client is just a thin
33
+ renderer that morphs between pages.
34
+
35
+ ## Getting Started
36
+
37
+ The framework adapter (React or Vue) is a peer dependency you install in your app —
38
+ the server package works against whichever you choose:
39
+
40
+ ```bash
41
+ # in your project root
42
+ bun add @inertiajs/react react react-dom # React
43
+ # or
44
+ bun add @inertiajs/vue3 vue # Vue
45
+ ```
46
+
47
+ `@zerotal/inertia` itself ships with the framework. If you're adding it to an
48
+ existing app, `bun add @zerotal/inertia`.
49
+
50
+ ## Register the provider
51
+
52
+ Add `InertiaProvider` to the providers array in `bootstrap/providers.ts`:
53
+
54
+ ```ts
55
+ // bootstrap/providers.ts
56
+ import { InertiaProvider } from "@zerotal/inertia";
57
+
58
+ const providers = [
59
+ // …your other providers
60
+ InertiaProvider,
61
+ ];
62
+
63
+ export default providers;
64
+ ```
65
+
66
+ That's the only wiring you need. Registering the provider switches on the following (in lifecycle order):
67
+
68
+ - `onRegister` — registers the `Router.inertia()` route macro so it's available before routes load.
69
+ - `onBooting` — auto-registers `InertiaMiddleware` via `app.useOnce()`, loads the HTML template and asset version into memory, resolves the pages directory, applies the history-encryption default, and (when `ssr: true`) registers `POST /__ssr`.
70
+ - `onBooted` — registers Inertia's dev build routine (alongside any other view layer's, so both keep rebuilding) and lazily registers the `make:page` and `inertia:build` CLI commands when running in console mode.
71
+
72
+ You do **not** add `InertiaMiddleware` to `.use()` manually (see [Middleware & Versioning](/docs/inertia/middleware)).
73
+
74
+ ## Configuration
75
+
76
+ Create `config/inertia.ts` with the `InertiaConfig()` helper (or `satisfies
77
+ InertiaConfigShape`). Every field has a default, so an empty `InertiaConfig({})` is valid:
78
+
79
+ ```ts
80
+ // config/inertia.ts
81
+ import { InertiaConfig } from "@zerotal/inertia";
82
+ import { env } from "zerotal";
83
+
84
+ export default InertiaConfig({
85
+ htmlTemplate: "./resources/app.html", // root template — must contain <!-- @inertia -->
86
+ version: env("ASSET_VERSION", "1"), // cache-bust string; bump on each deploy
87
+ assetsUrl: "/", // public URL prefix for built assets
88
+ pagesDir: "resources/js/pages", // where page components live
89
+ ssr: false, // set true to enable POST /__ssr (see SSR)
90
+ });
91
+ ```
92
+
93
+ | Field | Required | Default | Description |
94
+ | ---------------- | -------- | ------------------------ | ---------------------------------------------------------------------------------------------------- |
95
+ | `htmlTemplate` | no | `"./resources/app.html"` | Path to the root HTML template (must contain `<!-- @inertia -->`); falls back to a built-in default. |
96
+ | `version` | no | `"1"` | Asset version string embedded in every page object; bump on each deploy. |
97
+ | `assetsUrl` | no | `"/"` | Public URL prefix for built assets. |
98
+ | `pagesDir` | no | `"resources/js/pages"` | Directory (relative to the project root) where Inertia page components live. |
99
+ | `ssr` | no | `false` | Register `POST /__ssr` for endpoint SSR (requires a server renderer). |
100
+ | `encryptHistory` | no | `false` | Encrypt browser history state globally; per-page overrides via `Inertia.encryptHistory()`. |
101
+
102
+ ### HTML template
103
+
104
+ The template is loaded **once at boot**; `<!-- @inertia -->` is where the page
105
+ object and root `<div>` are injected on every response:
106
+
107
+ ```html
108
+ <!-- resources/app.html -->
109
+ <!DOCTYPE html>
110
+ <html>
111
+ <head>
112
+ <meta charset="utf-8" />
113
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
114
+ <title>My App</title>
115
+ <script type="module" src="/assets/app.js" defer></script>
116
+ </head>
117
+ <body>
118
+ <!-- @inertia -->
119
+ </body>
120
+ </html>
121
+ ```
122
+
123
+ > **Note** — If `htmlTemplate` is missing, `InertiaProvider` falls back to a built-in default
124
+ > template in development so the app still boots — but in production a missing template
125
+ > throws. Provide your own for real projects.
126
+
127
+ ## Your first page
128
+
129
+ A controller action calls `inertia(component, props)`:
130
+
131
+ ```ts
132
+ // app/controllers/DashboardController.ts
133
+ import type { HttpContext } from "zerotal";
134
+ import { inertia } from "@zerotal/inertia";
135
+ import { Post } from "../models/Post.ts";
136
+
137
+ export class DashboardController {
138
+ async index(ctx: HttpContext): Promise<void> {
139
+ const posts = await Post.query().latest().limit(5).get();
140
+ return inertia("Dashboard", { posts });
141
+ }
142
+ }
143
+ ```
144
+
145
+ `inertia()` reads the current request from context — it takes **no context
146
+ argument** — and sets the response as a side effect, so the action returns
147
+ `Promise<void>`. The component name (`"Dashboard"`) maps to
148
+ `resources/js/pages/Dashboard.tsx`.
149
+
150
+ ```tsx
151
+ // resources/js/pages/Dashboard.tsx
152
+ import { Link } from "@inertiajs/react";
153
+
154
+ interface Props {
155
+ posts: { id: number; slug: string; title: string }[];
156
+ auth: { user: { name: string } | null }; // from shared props
157
+ }
158
+
159
+ export default function Dashboard({ posts, auth }: Props) {
160
+ return (
161
+ <main>
162
+ <h1>Dashboard</h1>
163
+ {auth.user && <p>Welcome back, {auth.user.name}</p>}
164
+ {posts.map((post) => (
165
+ <Link key={post.id} href={`/posts/${post.slug}`}>
166
+ {post.title}
167
+ </Link>
168
+ ))}
169
+ </main>
170
+ );
171
+ }
172
+ ```
173
+
174
+ Note `auth` is available without the controller passing it — see
175
+ [Shared Props](/docs/inertia/props). Generate new pages with
176
+ [`make:page`](/docs/inertia/build#generating-a-page) and bundle them with
177
+ [`inertia:build`](/docs/inertia/build#building-assets).
178
+
179
+ ## Testing
180
+
181
+ Set your suite up once as described in [Testing](/docs/testing). An Inertia route
182
+ serves two different things depending on one header, so a test has to say which
183
+ one it wants.
184
+
185
+ **Send `X-Inertia: true` to get the page object** instead of the HTML shell. This
186
+ is the assertion you want in almost every route test — it checks the component
187
+ and its props without parsing markup:
188
+
189
+ ```typescript
190
+ // tests/http/dashboard.test.ts
191
+ import { test } from "bun:test";
192
+ import { createApp } from "../helpers.ts";
193
+
194
+ test("the dashboard renders with its stats", async () => {
195
+ const app = await createApp();
196
+
197
+ const res = await app.actingAs(user).get("/dashboard", { "X-Inertia": "true" });
198
+
199
+ res.assertJsonPath("component", "Dashboard");
200
+ res.assertJsonPath("props.stats.orders", 12);
201
+ await app.close();
202
+ });
203
+ ```
204
+
205
+ **Without that header you get the full HTML document** with the page object
206
+ embedded in a `data-page` attribute — right for asserting the first paint, wrong
207
+ for asserting props:
208
+
209
+ ```typescript
210
+ // tests/http/dashboard.test.ts
211
+ const res = await app.actingAs(user).get("/dashboard");
212
+
213
+ res.assertSee('<div id="app"');
214
+ res.assertHeader("Vary", "X-Inertia"); // the response varies by that header
215
+ ```
216
+
217
+ **Test partial reloads by the props they omit.** A partial reload that quietly
218
+ returns everything is a performance bug no page-level assertion catches:
219
+
220
+ ```typescript
221
+ // tests/http/dashboard.test.ts
222
+ const res = await app.actingAs(user).get("/dashboard", {
223
+ "X-Inertia": "true",
224
+ "X-Inertia-Partial-Data": "stats",
225
+ "X-Inertia-Partial-Component": "Dashboard",
226
+ });
227
+
228
+ res.assertJsonPath("props.stats.orders", 12);
229
+ const page = res.json<{ props: Record<string, unknown> }>();
230
+ expect(page.props.notifications).toBeUndefined(); // excluded, as asked
231
+ ```
232
+
233
+ **A version mismatch is a `409`, not an error.** It tells the client to reload so
234
+ it picks up new assets — worth a test if you set `ASSET_VERSION` on deploy:
235
+
236
+ ```typescript
237
+ // tests/http/dashboard.test.ts
238
+ const res = await app.get("/dashboard", { "X-Inertia": "true", "X-Inertia-Version": "stale" });
239
+
240
+ res.assertStatus(409);
241
+ res.assertHeader("X-Inertia-Location");
242
+ ```
243
+
244
+ > **Note** — For the client half — a component rendering, a form submitting, a
245
+ > deferred prop arriving — use [Browser Tests](/docs/testing/browser). These
246
+ > assertions stop at the boundary your server owns.
247
+
248
+ ## The rest of the guide
249
+
250
+ | Page | What it covers |
251
+ | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
252
+ | [Rendering Pages](/docs/inertia/rendering) | Return a page from a controller, choose a component, and control the response. |
253
+ | [Props](/docs/inertia/props) | Pass data to a page — eager, lazy, deferred, and merged props, plus the shared props every page receives. |
254
+ | [Middleware & Versioning](/docs/inertia/middleware) | The Inertia middleware, asset versioning, and forcing a full reload after a deploy. |
255
+ | [Server-Side Rendering](/docs/inertia/ssr) | Render the first paint on the server, and what changes when you do. |
256
+ | [DevTools](/docs/inertia/devtools) | Record each request's component, props, and timing for the Inertia DevTools extension. |
257
+ | [CLI & Build](/docs/inertia/build) | The page registry, the bundler pipeline, and building for production. |
258
+ | [Reference](/docs/inertia/references) | Every helper, prop type, and config key in one table. |
259
+
260
+ ## Next steps
261
+
262
+ - [Controllers](/docs/controllers) — where most `inertia()` calls live.
263
+ - [Routing](/docs/routing) — declare routes and the `Router.inertia()` macro targets.
264
+ - [Middleware](/docs/middleware) — how `InertiaMiddleware` slots into the pipeline.
265
+ - [Validator](/docs/validator) — the validation that feeds the `errors` shared prop and precognition.
266
+ - [Providers](/docs/providers) — the lifecycle hooks `InertiaProvider` builds on.
267
+ - [Pagination](/docs/pagination) — the paginators `merge()` and `scroll()` consume.
@@ -0,0 +1,81 @@
1
+ ---
2
+ title: Inertia Middleware & Versioning
3
+ description: The Inertia middleware, asset versioning, and forcing a full reload after a deploy.
4
+ ---
5
+
6
+ # Middleware & versioning
7
+
8
+ `InertiaMiddleware` implements the parts of the Inertia protocol that have to happen
9
+ at the HTTP layer. You don't register it manually — `InertiaProvider` adds it for you
10
+ via `useOnce()` during boot.
11
+
12
+ ## What the middleware does
13
+
14
+ `InertiaMiddleware` handles three protocol requirements on every response:
15
+
16
+ 1. **302 → 303 for non-GET redirects.** Inertia requires a 303 after a
17
+ POST/PUT/DELETE so the browser issues a `GET` on the redirect target instead of
18
+ replaying the form submission. Without this, redirect-after-submit flows break.
19
+
20
+ 2. **Asset-version mismatch → 409.** When the client's `X-Inertia-Version` header
21
+ differs from the server's current [asset version](#asset-versioning), the
22
+ middleware responds **409** with an `X-Inertia-Location` header. The Inertia
23
+ client reacts by doing a full page reload to pull the new assets — this is how
24
+ zero-downtime deploys avoid stale-bundle errors.
25
+
26
+ 3. **`Vary: X-Inertia` on every response.** Prevents a browser/CDN cache from serving
27
+ a JSON navigation response where an HTML document is expected (or vice versa) on
28
+ Back/Refresh.
29
+
30
+ ## Automatic registration
31
+
32
+ `InertiaProvider.onBooting()` calls `this.app.useOnce(InertiaMiddleware)`, so simply
33
+ registering the provider is enough:
34
+
35
+ ```ts
36
+ // bootstrap/providers.ts — this is all you need
37
+ import { InertiaProvider } from "@zerotal/inertia";
38
+
39
+ export default [
40
+ // …auth, session providers…
41
+ InertiaProvider,
42
+ ];
43
+ ```
44
+
45
+ You do **not** add `InertiaMiddleware` to `Application.create().use([...])` — doing so
46
+ would just register it twice (and `useOnce` guards against that anyway).
47
+
48
+ > **Note** — Ordering & `ctx.user`. Shared props read `ctx.user` at the moment `inertia()`
49
+ > runs _inside your controller_ — which is after the entire middleware pipeline. So
50
+ > the authenticated user is always populated by the time props are built; you don't
51
+ > need to hand-order `InertiaMiddleware` relative to auth.
52
+
53
+ ## Asset versioning
54
+
55
+ The asset version is a string sent as part of every page object. When it changes, the
56
+ client knows its cached bundle is stale and triggers a reload (the 409 flow above).
57
+
58
+ Set a baseline in [config](/docs/inertia#configuration) (`version`), or compute one at boot
59
+ from the actual bundle so it changes automatically on every deploy:
60
+
61
+ ```ts
62
+ // in a ServiceProvider.onBooting()
63
+ import { setAssetVersion } from "@zerotal/inertia";
64
+
65
+ const hash = Bun.hash(await Bun.file("public/assets/app.js").text()).toString(16);
66
+ setAssetVersion(hash);
67
+ ```
68
+
69
+ | Function | Purpose |
70
+ | -------------------- | -------------------------------------------------- |
71
+ | `setAssetVersion(v)` | Set the current version (called by the provider). |
72
+ | `assetVersion()` | Read the current version (embedded in every page). |
73
+
74
+ Hashing the built bundle is the most robust option: the version is guaranteed to
75
+ change exactly when the client code changes, so users always reload onto matching
76
+ assets after a deploy.
77
+
78
+ ## Next steps
79
+
80
+ - [Inertia overview](/docs/inertia) — the guide's front page and the rest of the sections.
81
+ - [Reference](/docs/inertia/references) — the full API surface in one table.