@zerotal/arch 1.7.4 → 1.7.5

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 (119) hide show
  1. package/CHANGELOG.md +34 -2
  2. package/README.md +4 -2
  3. package/docs/about.md +16 -16
  4. package/docs/admin/actions.md +9 -9
  5. package/docs/admin/auth.md +2 -2
  6. package/docs/admin/dashboard.md +5 -5
  7. package/docs/admin/extending-ui.md +12 -12
  8. package/docs/admin/extending.md +16 -7
  9. package/docs/admin/forms.md +4 -4
  10. package/docs/admin/index.md +2 -2
  11. package/docs/admin/operations.md +9 -9
  12. package/docs/admin/resources.md +3 -3
  13. package/docs/admin/structure.md +6 -6
  14. package/docs/admin/tables.md +8 -8
  15. package/docs/admin/testing.md +4 -4
  16. package/docs/ai.md +118 -11
  17. package/docs/application.md +12 -12
  18. package/docs/arch.md +25 -3
  19. package/docs/assets.md +11 -11
  20. package/docs/audit.md +13 -13
  21. package/docs/authentication.md +39 -39
  22. package/docs/authorization.md +13 -13
  23. package/docs/broadcasting/channels.md +2 -2
  24. package/docs/broadcasting/client.md +2 -2
  25. package/docs/broadcasting/events.md +9 -10
  26. package/docs/broadcasting/index.md +1 -1
  27. package/docs/broadcasting/testing.md +5 -5
  28. package/docs/cache.md +15 -13
  29. package/docs/carbon.md +35 -35
  30. package/docs/changelog.md +105 -1
  31. package/docs/client/index.md +13 -13
  32. package/docs/commands.md +4 -4
  33. package/docs/components.md +116 -115
  34. package/docs/config-system.md +4 -4
  35. package/docs/container.md +27 -27
  36. package/docs/context.md +27 -27
  37. package/docs/contributing.md +28 -0
  38. package/docs/controllers.md +10 -10
  39. package/docs/conventions.md +23 -15
  40. package/docs/cookies.md +3 -3
  41. package/docs/csrf.md +6 -6
  42. package/docs/database.md +22 -17
  43. package/docs/deployment.md +2 -2
  44. package/docs/devtools.md +16 -16
  45. package/docs/email-verification.md +8 -8
  46. package/docs/encryption.md +7 -7
  47. package/docs/errors.md +6 -6
  48. package/docs/events.md +12 -11
  49. package/docs/flow/components.md +32 -32
  50. package/docs/flow/decorators.md +18 -18
  51. package/docs/flow/events.md +16 -16
  52. package/docs/flow/forms.md +17 -17
  53. package/docs/flow/icons.md +5 -5
  54. package/docs/flow/index.md +20 -20
  55. package/docs/flow/layouts.md +35 -35
  56. package/docs/flow/lifecycle.md +16 -16
  57. package/docs/flow/models.md +9 -9
  58. package/docs/flow/pagination.md +8 -8
  59. package/docs/flow/performance.md +7 -7
  60. package/docs/flow/references.md +3 -3
  61. package/docs/flow/routing.md +26 -26
  62. package/docs/flow/testing.md +20 -20
  63. package/docs/getting-started.md +18 -13
  64. package/docs/health.md +17 -8
  65. package/docs/helpers.md +17 -17
  66. package/docs/i18n.md +10 -10
  67. package/docs/inertia/devtools.md +4 -4
  68. package/docs/inertia/index.md +6 -6
  69. package/docs/inertia/props.md +21 -21
  70. package/docs/inertia/rendering.md +9 -9
  71. package/docs/inertia/ssr.md +2 -2
  72. package/docs/lifecycle.md +2 -2
  73. package/docs/lock.md +11 -11
  74. package/docs/logger.md +12 -12
  75. package/docs/media.md +22 -22
  76. package/docs/middleware.md +17 -17
  77. package/docs/migrations.md +13 -13
  78. package/docs/monitor.md +6 -6
  79. package/docs/notifications.md +24 -24
  80. package/docs/orm/casts.md +14 -14
  81. package/docs/orm/factories.md +8 -8
  82. package/docs/orm/index.md +22 -20
  83. package/docs/orm/lifecycle.md +13 -13
  84. package/docs/orm/queries.md +28 -28
  85. package/docs/orm/relationships.md +16 -16
  86. package/docs/orm/serialization.md +11 -11
  87. package/docs/package-development.md +10 -10
  88. package/docs/pagination.md +14 -14
  89. package/docs/password-reset.md +6 -6
  90. package/docs/providers.md +11 -11
  91. package/docs/query-builder.md +18 -18
  92. package/docs/queue.md +14 -12
  93. package/docs/rate-limiting.md +6 -6
  94. package/docs/responses.md +14 -14
  95. package/docs/roles-and-2fa.md +9 -9
  96. package/docs/routing.md +44 -44
  97. package/docs/scaffolding.md +1 -1
  98. package/docs/scheduler.md +10 -10
  99. package/docs/seeding.md +6 -6
  100. package/docs/session.md +16 -14
  101. package/docs/social.md +14 -14
  102. package/docs/storage.md +25 -13
  103. package/docs/structure.md +2 -2
  104. package/docs/support-policy.md +23 -10
  105. package/docs/telemetry.md +12 -12
  106. package/docs/tenancy.md +34 -40
  107. package/docs/testing/browser.md +6 -6
  108. package/docs/testing/console.md +3 -3
  109. package/docs/testing/database.md +10 -10
  110. package/docs/testing/flow-browser.md +6 -6
  111. package/docs/testing/http.md +12 -12
  112. package/docs/testing/index.md +3 -3
  113. package/docs/testing/mocking.md +8 -8
  114. package/docs/upgrade.md +1 -1
  115. package/docs/validator.md +19 -19
  116. package/docs/view.md +14 -14
  117. package/package.json +4 -4
  118. package/src/index.ts +25 -1
  119. package/src/tools/searchDocs.ts +7 -0
package/docs/helpers.md CHANGED
@@ -23,7 +23,7 @@ Read an environment variable with an optional, type-coerced fallback. This is th
23
23
  canonical way to read env values — every config file uses it. The return type
24
24
  follows the fallback's type.
25
25
 
26
- ```typescript
26
+ ```typescript fragment
27
27
  // config/app.ts
28
28
  env("APP_NAME", "Zerotal App"); // string
29
29
  env("APP_DEBUG", false); // boolean — coerces 'true' / '1'
@@ -36,7 +36,7 @@ env("APP_KEY"); // string | undefined — no fallback
36
36
  Read a variable that must exist. Throws a `ConfigError` at boot if it's missing —
37
37
  use it for secrets your app cannot run without.
38
38
 
39
- ```typescript
39
+ ```typescript fragment
40
40
  // config/app.ts
41
41
  const key = requireEnv("APP_KEY"); // throws ConfigError if unset
42
42
  ```
@@ -47,7 +47,7 @@ Resolve a path relative to the project root (`process.cwd()`), regardless of whi
47
47
  file calls it. Use it when declaring route files so paths don't depend on the
48
48
  caller's directory.
49
49
 
50
- ```typescript
50
+ ```typescript fragment
51
51
  // bootstrap/app.ts
52
52
  Application.create({ providers })
53
53
  .routing({ web: basePath("routes/web.ts") })
@@ -61,7 +61,7 @@ managed `zt.ts`; you rarely call it yourself. `serve`/`start`/`s` → `web`,
61
61
  `worker`/`queue:work` → `worker`, anything else → `console`. A no-op if `APP_ENV`
62
62
  is already a valid runtime mode.
63
63
 
64
- ```typescript
64
+ ```typescript fragment
65
65
  // zt.ts
66
66
  setAppEnv(process.argv[2]);
67
67
  const { default: app } = await import("./bootstrap/app.ts");
@@ -73,7 +73,7 @@ const { default: app } = await import("./bootstrap/app.ts");
73
73
 
74
74
  Read (and write) loaded configuration by dot-path, anywhere after boot.
75
75
 
76
- ```typescript
76
+ ```typescript fragment
77
77
  // in a controller
78
78
  config("app.name"); // string — typed from the registered config shape
79
79
  config("app.port", 3000); // number — fallback must match the path's type
@@ -96,7 +96,7 @@ Reach the current request's `HttpContext` from anywhere in the async call chain
96
96
  no thread-through required. With a key, it reads a single merged input value (route
97
97
  params, then parsed body, then query string).
98
98
 
99
- ```typescript
99
+ ```typescript fragment
100
100
  // in a controller
101
101
  request(); // the HttpContext
102
102
  request("email"); // string | undefined — input named 'email'
@@ -114,7 +114,7 @@ request<number>("page", 1); // typed input with a fallback
114
114
  Run a side effect on a value and return the value unchanged — perfect for emitting
115
115
  an event or logging in the middle of a chain without breaking it.
116
116
 
117
- ```typescript
117
+ ```typescript fragment
118
118
  // in a controller
119
119
  return tap(await User.create(data), (user) => Events.emit(new UserRegistered(user.id)));
120
120
 
@@ -128,7 +128,7 @@ return await tapAsync(await User.create(data), async (user) => {
128
128
  The sibling of `tap` — pass a value through a transformer and return the _result_.
129
129
  Use `pipe` when the value should change, `tap` when it shouldn't.
130
130
 
131
- ```typescript
131
+ ```typescript fragment
132
132
  // in a controller
133
133
  const slug = pipe(post.title, (t) => t.toLowerCase().replace(/\s+/g, "-"));
134
134
  const hashed = await pipeAsync(password, (p) => Hash.make(p));
@@ -140,7 +140,7 @@ Run a callback and fall back to a value (or a function of the caught error) inst
140
140
  of throwing. `rescue` awaits; `rescueSync` is for hot paths that can't await
141
141
  (JSON parsing, attribute decoding).
142
142
 
143
- ```typescript
143
+ ```typescript fragment
144
144
  // in a controller
145
145
  const price = await rescue(() => stripe.getPrice(id), 0);
146
146
  const user = await rescue(
@@ -160,7 +160,7 @@ Safely read a deeply nested value by dot-notation, returning a default when any
160
160
  segment is absent. Built for untyped JSON — webhooks, third-party API responses —
161
161
  where optional chaining gets unwieldy. Numeric segments index into arrays.
162
162
 
163
- ```typescript
163
+ ```typescript fragment
164
164
  // in a webhook handler
165
165
  data_get(payload, "user.address.city"); // 'Cape Town' or undefined
166
166
  data_get(payload, "items.0.price", 0); // first item's price, or 0
@@ -171,7 +171,7 @@ data_get(payload, "items.0.price", 0); // first item's price, or 0
171
171
  `Str` is a namespace of pure string utilities. The common case-conversions are
172
172
  also exported individually (`camelCase`, `snakeCase`).
173
173
 
174
- ```typescript
174
+ ```typescript fragment
175
175
  // in a controller
176
176
  Str.camelCase("user_id"); // 'userId'
177
177
  Str.snakeCase("userId"); // 'user_id'
@@ -270,7 +270,7 @@ flat, and `DeepPartial<XConfigShape>` when it nests — both satisfy the package
270
270
  An array in the override replaces the base array entirely. It is **never**
271
271
  concatenated, de-duplicated, or merged element-by-element:
272
272
 
273
- ```typescript
273
+ ```typescript fragment
274
274
  // in a config factory
275
275
  deepMerge({ hosts: ["a", "b"] }, { hosts: ["c"] });
276
276
  // → { hosts: ['c'] } (not ['a','b','c'], not ['c','b'])
@@ -301,7 +301,7 @@ matches how you want overrides to behave.
301
301
  Wrap any value to chain `.pipe()` transforms and `.tap()` side effects, then unwrap
302
302
  with `.get()`. Useful for readable builder-style code over a plain value.
303
303
 
304
- ```typescript
304
+ ```typescript fragment
305
305
  // in a controller
306
306
  const user = fluent(await User.find(id))
307
307
  .tap((u) => log(`loaded ${u.email}`))
@@ -314,7 +314,7 @@ Wrap an array in a `Collection` for chainable, immutable transformations — `ma
314
314
  `filter`, `reduce`, `groupBy`, `pluck`, `sum`, `first`, `unique`, and more — a
315
315
  fluent collection pipeline.
316
316
 
317
- ```typescript
317
+ ```typescript fragment
318
318
  // in a controller
319
319
  const topNames = collect(orders)
320
320
  .filter((o) => o.paid)
@@ -332,7 +332,7 @@ These build and send the HTTP response for the current request. The terminal
332
332
  helpers (`json`, `view`, `html`, `markdown`, `file`) set `ctx.response` directly;
333
333
  `redirect()` and `redirectTo()` return a chainable `ResponseBuilder`.
334
334
 
335
- ```typescript
335
+ ```typescript fragment
336
336
  // in a controller
337
337
  import { json, view, html, markdown, redirect, redirectTo, abort } from "zerotal";
338
338
 
@@ -352,7 +352,7 @@ redirectTo("posts.show", { id }); // redirect to a named route
352
352
  `redirect()` and `redirect().back()` return a `ResponseBuilder` that lets you flash
353
353
  data and messages onto the redirect:
354
354
 
355
- ```typescript
355
+ ```typescript fragment
356
356
  // in a controller
357
357
  return redirect("/posts").withSuccess("Post created.").with("highlight", post.id);
358
358
 
@@ -366,7 +366,7 @@ return redirect().back().withErrors({ title: "Title is required." });
366
366
 
367
367
  `abort()` throws a framework error that the exception handler renders:
368
368
 
369
- ```typescript
369
+ ```typescript fragment
370
370
  // in a controller
371
371
  abort("Something went wrong."); // → 500 with a message
372
372
  abort(403, "You can't do that."); // status + message
package/docs/i18n.md CHANGED
@@ -101,7 +101,7 @@ themselves. An unmatched lookup returns the key, and the key is the English.
101
101
  `LocaleMiddleware` resolves the locale for every request and exposes `ctx.locale`
102
102
  and `ctx.__()` on the request context:
103
103
 
104
- ```typescript
104
+ ```typescript fragment
105
105
  // in a controller
106
106
  async show(ctx: HttpContext) {
107
107
  ctx.__("Hello, {name}!", { name: "Alice" }); // active locale
@@ -140,7 +140,7 @@ Lang.translate("Sign in"); // the facade, when you want the instance
140
140
  A React interface cannot reach the server's `Translator`, so the active locale's
141
141
  catalog travels to it as a shared Inertia prop:
142
142
 
143
- ```typescript
143
+ ```typescript fragment
144
144
  // bootstrap/app.ts
145
145
  import { share } from "@zerotal/inertia";
146
146
  import fr from "../resources/lang/fr.json";
@@ -157,7 +157,7 @@ Client-side, keep the catalog in module state and hand it to the translator from
157
157
  the component in, which is what makes the first render after a language change
158
158
  come out in the new language:
159
159
 
160
- ```tsx
160
+ ```tsx fragment
161
161
  // resources/js/app.tsx
162
162
  createInertiaApp({
163
163
  resolve: async (name, incoming) => {
@@ -179,7 +179,7 @@ guarantees the assignment has run:
179
179
 
180
180
  A component then calls it with no import and no hook:
181
181
 
182
- ```tsx
182
+ ```tsx fragment
183
183
  // resources/js/pages/login.tsx
184
184
  <TextField label={__("Email")} type="email" />
185
185
  ```
@@ -267,7 +267,7 @@ English strings as well — the ambiguity was there before the translator found
267
267
  Enum values and column names cannot be passed to `__()` — `in_progress` is not
268
268
  English. Map them to English first, then translate the result:
269
269
 
270
- ```typescript
270
+ ```typescript fragment
271
271
  const STATUS_LABEL: Record<string, string> = {
272
272
  backlog: "Backlog",
273
273
  in_progress: "In progress",
@@ -310,7 +310,7 @@ things.
310
310
  **Persist a user's choice** by writing the `locale` cookie; the `cookie` resolver
311
311
  picks it up on every subsequent request:
312
312
 
313
- ```typescript
313
+ ```typescript fragment
314
314
  // in a controller — save the visitor's language choice
315
315
  async setLocale(ctx: HttpContext) {
316
316
  const { locale } = await ctx.body<{ locale: string }>();
@@ -328,7 +328,7 @@ async setLocale(ctx: HttpContext) {
328
328
  user — by running code inside `I18nContext.run()`. The `Lang` facade and `__()`
329
329
  helper use the supplied locale for the duration of the callback:
330
330
 
331
- ```typescript
331
+ ```typescript fragment
332
332
  // in a controller
333
333
  import { I18nContext, __ } from "@zerotal/i18n";
334
334
 
@@ -355,7 +355,7 @@ belonging to whoever triggered the job was never the right answer anyway. Mail
355
355
  should arrive in the language of the person opening it, so pass the locale
356
356
  explicitly as the third argument:
357
357
 
358
- ```typescript
358
+ ```typescript fragment
359
359
  // app/notifications/IssueAssignedNotification.ts
360
360
  toMail(notifiable: Notifiable): MailMessage {
361
361
  const recipient = notifiable as { name?: string; locale?: string | null };
@@ -389,7 +389,7 @@ See [Validator](/docs/validator) for how the validator itself reports errors.
389
389
 
390
390
  ## Using __ in JSX views
391
391
 
392
- ```tsx
392
+ ```tsx fragment
393
393
  // app/views/PostCard.tsx
394
394
  import { __ } from "@zerotal/i18n";
395
395
 
@@ -469,7 +469,7 @@ test("reports the strings each locale still needs", async () => {
469
469
  separately — a correct catalogue served under the wrong locale looks like a
470
470
  missing translation:
471
471
 
472
- ```typescript
472
+ ```typescript fragment
473
473
  // tests/http/locale.test.ts
474
474
  const res = await app.get("/", { "Accept-Language": "fr-CA,fr;q=0.9" });
475
475
 
@@ -84,7 +84,7 @@ INERTIA_DEVTOOLS_ENABLED=true # on — see the warning below
84
84
 
85
85
  Or configure it:
86
86
 
87
- ```typescript
87
+ ```typescript fragment
88
88
  // config/inertia.ts
89
89
  import { InertiaConfig } from "zerotal/inertia";
90
90
 
@@ -129,7 +129,7 @@ Matching is a case-insensitive substring, so `password` also covers
129
129
 
130
130
  Add your own:
131
131
 
132
- ```typescript
132
+ ```typescript fragment
133
133
  // config/inertia.ts
134
134
  export default InertiaConfig({
135
135
  devtools: {
@@ -148,7 +148,7 @@ request.
148
148
  The read API never records itself. Add anything else that would bury the
149
149
  timeline:
150
150
 
151
- ```typescript
151
+ ```typescript fragment
152
152
  // config/inertia.ts
153
153
  export default InertiaConfig({
154
154
  devtools: { except: ["/health", "/metrics"] },
@@ -157,7 +157,7 @@ export default InertiaConfig({
157
157
 
158
158
  ### Gating a shared environment
159
159
 
160
- ```typescript
160
+ ```typescript fragment
161
161
  // config/inertia.ts
162
162
  export default InertiaConfig({
163
163
  devtools: {
@@ -128,7 +128,7 @@ object and root `<div>` are injected on every response:
128
128
 
129
129
  A controller action calls `inertia(component, props)`:
130
130
 
131
- ```ts
131
+ ```ts fragment
132
132
  // app/controllers/DashboardController.ts
133
133
  import type { HttpContext } from "zerotal";
134
134
  import { inertia } from "@zerotal/inertia";
@@ -147,7 +147,7 @@ argument** — and sets the response as a side effect, so the action returns
147
147
  `Promise<void>`. The component name (`"Dashboard"`) maps to
148
148
  `resources/js/pages/Dashboard.tsx`.
149
149
 
150
- ```tsx
150
+ ```tsx fragment
151
151
  // resources/js/pages/Dashboard.tsx
152
152
  import { Link } from "@inertiajs/react";
153
153
 
@@ -191,7 +191,7 @@ one it wants.
191
191
  is the assertion you want in almost every route test — it checks the component
192
192
  and its props without parsing markup:
193
193
 
194
- ```typescript
194
+ ```typescript fragment
195
195
  // tests/http/dashboard.test.ts
196
196
  import { test } from "bun:test";
197
197
  import { createApp } from "../helpers.ts";
@@ -211,7 +211,7 @@ test("the dashboard renders with its stats", async () => {
211
211
  embedded in a `data-page` attribute — right for asserting the first paint, wrong
212
212
  for asserting props:
213
213
 
214
- ```typescript
214
+ ```typescript fragment
215
215
  // tests/http/dashboard.test.ts
216
216
  const res = await app.actingAs(user).get("/dashboard");
217
217
 
@@ -222,7 +222,7 @@ res.assertHeader("Vary", "X-Inertia"); // the response varies by that header
222
222
  **Test partial reloads by the props they omit.** A partial reload that quietly
223
223
  returns everything is a performance bug no page-level assertion catches:
224
224
 
225
- ```typescript
225
+ ```typescript fragment
226
226
  // tests/http/dashboard.test.ts
227
227
  const res = await app.actingAs(user).get("/dashboard", {
228
228
  "X-Inertia": "true",
@@ -238,7 +238,7 @@ expect(page.props.notifications).toBeUndefined(); // excluded, as asked
238
238
  **A version mismatch is a `409`, not an error.** It tells the client to reload so
239
239
  it picks up new assets — worth a test if you set `ASSET_VERSION` on deploy:
240
240
 
241
- ```typescript
241
+ ```typescript fragment
242
242
  // tests/http/dashboard.test.ts
243
243
  const res = await app.get("/dashboard", { "X-Inertia": "true", "X-Inertia-Version": "stale" });
244
244
 
@@ -32,7 +32,7 @@ The server evaluates and returns just those props; the client keeps the rest. Ze
32
32
  `X-Inertia-Partial-Data` / `X-Inertia-Partial-Except` / `X-Inertia-Partial-Component` headers
33
33
  automatically — you don't write any special code for the route, you just make props lazy.
34
34
 
35
- ```tsx
35
+ ```tsx fragment
36
36
  // client
37
37
  router.reload({ only: ["users"] });
38
38
  ```
@@ -41,7 +41,7 @@ router.reload({ only: ["users"] });
41
41
 
42
42
  Wrap optional data in a function so it's only evaluated when actually included:
43
43
 
44
- ```ts
44
+ ```ts fragment
45
45
  // in a controller
46
46
  return inertia("Users/Index", {
47
47
  users: () => User.all(), // evaluated every full visit, and on partial reloads that ask for it
@@ -57,7 +57,7 @@ it from a partial reload also skips the query.
57
57
  `optional(fn)` — never sent on a normal visit; only when explicitly requested via `only`. Ideal for
58
58
  expensive data the page can load on demand.
59
59
 
60
- ```ts
60
+ ```ts fragment
61
61
  // in a controller
62
62
  return inertia("Users/Index", {
63
63
  users: optional(() => User.all()), // only when reloaded with only: ["users"]
@@ -83,7 +83,7 @@ shared `errors` bag uses this internally.)
83
83
  request — great for below-the-fold or slow data. Group props to control parallelism (each group is
84
84
  one request).
85
85
 
86
- ```ts
86
+ ```ts fragment
87
87
  // in a controller
88
88
  return inertia("Users/Index", {
89
89
  users: () => User.all(),
@@ -96,7 +96,7 @@ return inertia("Users/Index", {
96
96
  The first response carries a `deferredProps` map; the client then partial-reloads each group. On the
97
97
  client, wrap the UI in `<Deferred>`:
98
98
 
99
- ```tsx
99
+ ```tsx fragment
100
100
  // in a page component
101
101
  import { Deferred } from "@inertiajs/react";
102
102
 
@@ -110,7 +110,7 @@ import { Deferred } from "@inertiajs/react";
110
110
  Pass `{ rescue: true }` so a thrown error is swallowed and the key reported in `rescuedProps` (the
111
111
  client renders the `<Deferred rescue>` slot) instead of failing the whole response:
112
112
 
113
- ```ts
113
+ ```ts fragment
114
114
  // in a controller
115
115
  permissions: defer(() => Permission.all(), "default", { rescue: true }),
116
116
  ```
@@ -121,7 +121,7 @@ By default a reloaded prop _replaces_ the client value. `merge()` / `deepMerge()
121
121
  **combine** the new data with what it already has — the basis for paginated "load more" lists. Merging
122
122
  only happens on partial reloads (full visits always replace).
123
123
 
124
- ```ts
124
+ ```ts fragment
125
125
  // in a controller
126
126
  return inertia("Feed", {
127
127
  posts: merge(() => Post.paginate(15, page)), // append at root
@@ -130,7 +130,7 @@ return inertia("Feed", {
130
130
 
131
131
  Chainable targeting:
132
132
 
133
- ```ts
133
+ ```ts fragment
134
134
  // in a controller
135
135
  merge(users).append("data").matchOn("data.id"); // append to users.data, replace items matching id
136
136
  merge(items).prepend(); // prepend at root
@@ -147,7 +147,7 @@ before merging fresh data (e.g. on a new search) — Zerotal honors the `X-Inert
147
147
  paginator's `data` array and emits a `scrollProps` entry describing the current/next/previous page,
148
148
  so the client knows when (and which way) to load more:
149
149
 
150
- ```ts
150
+ ```ts fragment
151
151
  // app/controllers/PostController.ts
152
152
  import { inertia, scroll } from "@zerotal/inertia";
153
153
 
@@ -176,7 +176,7 @@ This produces a page object like:
176
176
  `X-Inertia-Infinite-Scroll-Merge-Intent: prepend` and Zerotal prepends the new page instead of
177
177
  appending — no controller change needed. On the client:
178
178
 
179
- ```tsx
179
+ ```tsx fragment
180
180
  // in a page component
181
181
  import { InfiniteScroll } from "@inertiajs/react";
182
182
 
@@ -193,7 +193,7 @@ Chain `.once()` onto an optional/merge/defer prop so it's resolved a single time
193
193
  client across navigations. The client sends `X-Inertia-Except-Once-Props` with the keys it already
194
194
  holds; the server skips re-resolving them.
195
195
 
196
- ```ts
196
+ ```ts fragment
197
197
  // in a controller
198
198
  return inertia("Billing/Plans", {
199
199
  plans: optional(() => Plan.all()).once(),
@@ -231,7 +231,7 @@ export default InertiaConfig({ encryptHistory: true });
231
231
  `Inertia.location(url)` performs a full-page visit to an external URL — a `409` with
232
232
  `X-Inertia-Location` for Inertia requests, a `302` otherwise:
233
233
 
234
- ```ts
234
+ ```ts fragment
235
235
  // in a controller
236
236
  return Inertia.location("https://billing.stripe.com/session/abc");
237
237
  ```
@@ -247,7 +247,7 @@ side effects** — perfect for live, inline validation. Register `PrecognitionMi
247
247
  `FormRequest`; when the client sends `Precognition: true`, `FormRequest.validate()` short-circuits
248
248
  with a `204` (valid) or `422` (errors), optionally limited to the fields in `Precognition-Validate-Only`.
249
249
 
250
- ```ts
250
+ ```ts fragment
251
251
  // bootstrap — register PrecognitionMiddleware globally
252
252
  import { PrecognitionMiddleware } from "@zerotal/inertia";
253
253
 
@@ -302,7 +302,7 @@ same key wins.
302
302
 
303
303
  ### What's provided
304
304
 
305
- ```ts
305
+ ```ts fragment
306
306
  // the shared bag sharedProps() returns
307
307
  {
308
308
  auth: {
@@ -325,7 +325,7 @@ that bag. The other shared props (`auth`/`flash`/`old`) are ordinary props, so a
325
325
 
326
326
  Every page component can read these without the controller passing them:
327
327
 
328
- ```tsx
328
+ ```tsx fragment
329
329
  // resources/js/pages/Page.tsx
330
330
  import { usePage } from "@inertiajs/react";
331
331
 
@@ -365,7 +365,7 @@ Register props once — typically in a provider's boot or in middleware — and
365
365
  every page. Values may be plain values, factory functions (evaluated lazily per request), or any
366
366
  [prop wrapper](#data-props):
367
367
 
368
- ```ts
368
+ ```ts fragment
369
369
  // in a provider's boot or middleware
370
370
  import { Inertia } from "@zerotal/inertia";
371
371
 
@@ -384,7 +384,7 @@ over during instant visits.
384
384
 
385
385
  #### Merge in the controller
386
386
 
387
- ```ts
387
+ ```ts fragment
388
388
  // in a controller
389
389
  return inertia("Dashboard", {
390
390
  notifications: await Notification.query().where("user_id", user.id).unread().get(),
@@ -397,7 +397,7 @@ return inertia("Dashboard", {
397
397
  When several pages need the same extra data, set it on the context in a middleware
398
398
  and read it back in the controller — keeping the controller body clean:
399
399
 
400
- ```ts
400
+ ```ts fragment
401
401
  // in a middleware:
402
402
  ctx.setInternal("unreadCount", await Notification.unreadCount(ctx.user!.id));
403
403
 
@@ -415,7 +415,7 @@ displays the value on every page.
415
415
  The props a controller passes are checked against the props the page component
416
416
  declares:
417
417
 
418
- ```tsx
418
+ ```tsx fragment
419
419
  // resources/js/pages/Posts/Show.tsx
420
420
  interface Props {
421
421
  post: Post;
@@ -425,7 +425,7 @@ interface Props {
425
425
  export default function Show({ post, related, stats }: Props) { … }
426
426
  ```
427
427
 
428
- ```ts
428
+ ```ts fragment
429
429
  // in a controller
430
430
  return Inertia.render("Posts/Show", {
431
431
  post,
@@ -455,7 +455,7 @@ the component receives `Post[]` — so each prop accepts its value, a factory fo
455
455
  it, or a wrapper carrying it, and the wrapper's payload is checked against the
456
456
  prop it fills:
457
457
 
458
- ```ts
458
+ ```ts fragment
459
459
  Inertia.render("Posts/Show", { post, related: merge(() => [1, 2]) });
460
460
  // ✗ number[] is not Post[]
461
461
  ```
@@ -11,7 +11,7 @@ routes, and how redirects behave.
11
11
 
12
12
  ## The inertia helper
13
13
 
14
- ```ts
14
+ ```ts fragment
15
15
  function inertia(component: PageName, props?: RenderProps): Promise<void>;
16
16
  ```
17
17
 
@@ -20,7 +20,7 @@ Call `inertia()` from any controller action. It reads the active request from
20
20
  it sets `ctx.response` as a side effect. It is **async** (it resolves lazy/deferred props), so it
21
21
  returns `Promise<void>` — always `return inertia(...)` (or `await` it):
22
22
 
23
- ```ts
23
+ ```ts fragment
24
24
  // app/controllers/PostController.ts
25
25
  import type { HttpContext } from "zerotal";
26
26
  import { inertia } from "@zerotal/inertia";
@@ -61,7 +61,7 @@ action.
61
61
  Props can be more than plain values. Wrap them to make them lazy, optional, deferred, or mergeable —
62
62
  the foundation for partial reloads, "load more" lists, and deferred content:
63
63
 
64
- ```ts
64
+ ```ts fragment
65
65
  // in a controller
66
66
  import { inertia, optional, defer, merge } from "@zerotal/inertia";
67
67
 
@@ -130,7 +130,7 @@ variants separately. The page object always includes the current `url` and asset
130
130
  For pages that need no controller logic (marketing pages, static dashboards), render
131
131
  straight from the route with the `Router.inertia()` macro (added by the package):
132
132
 
133
- ```ts
133
+ ```ts fragment
134
134
  // routes/web.ts
135
135
  import { Router } from "zerotal";
136
136
 
@@ -142,7 +142,7 @@ Router.inertia("/admin", "Admin/Dashboard", [AuthMiddleware]); // middleware sho
142
142
  The third argument is polymorphic: pass a **props object**, or pass a **middleware
143
143
  array** directly as a shorthand. To use both, pass props third and middleware fourth:
144
144
 
145
- ```ts
145
+ ```ts fragment
146
146
  // routes/web.ts
147
147
  Router.inertia("/admin", "Admin/Dashboard", { title: "Admin" }, [AuthMiddleware]);
148
148
  ```
@@ -155,7 +155,7 @@ someone clicks, not when someone builds.
155
155
 
156
156
  Name the route instead, and let the URL be derived:
157
157
 
158
- ```tsx
158
+ ```tsx fragment
159
159
  import { Link } from "@inertiajs/react";
160
160
 
161
161
  <Link href={route("posts.show", { slug: post.slug })}>{post.title}</Link>
@@ -174,7 +174,7 @@ A form's action is the same kind of string as a link's `href`, and gets the same
174
174
  treatment. `useForm()` and `router` both take a URL, so hand them one that was built
175
175
  from the route name:
176
176
 
177
- ```tsx
177
+ ```tsx fragment
178
178
  import { useForm, router } from "@inertiajs/react";
179
179
 
180
180
  export default function Edit({ post }: Props) {
@@ -232,7 +232,7 @@ After a non-GET action (a form POST/PUT/DELETE), redirect as usual — return a
232
232
  [`InertiaMiddleware`](/docs/inertia/middleware) upgrades it to a **303** so the
233
233
  browser issues a GET on the target instead of replaying the form:
234
234
 
235
- ```ts
235
+ ```ts fragment
236
236
  // in a controller
237
237
  async store(ctx: HttpContext): Promise<void> {
238
238
  const post = await Post.create(await ctx.body());
@@ -251,7 +251,7 @@ To send the browser to an external URL (or force a full-page visit), use `Inerti
251
251
  an Inertia request it returns a `409` + `X-Inertia-Location` so the client does a `window.location`
252
252
  visit; on a normal request it's a plain `302`:
253
253
 
254
- ```ts
254
+ ```ts fragment
255
255
  // in a controller
256
256
  import { Inertia } from "@zerotal/inertia";
257
257
 
@@ -60,7 +60,7 @@ streams the React output between the template's HTML prefix and suffix:
60
60
 
61
61
  Swap `inertia()` → `inertiaStream()` and `await` it — nothing else changes:
62
62
 
63
- ```ts
63
+ ```ts fragment
64
64
  // app/controllers/PostController.ts
65
65
  import { inertiaStream } from "@zerotal/inertia";
66
66
 
@@ -83,7 +83,7 @@ traversal (`..` or a leading `/`).
83
83
 
84
84
  ### inertia vs. inertiaStream
85
85
 
86
- | | `inertia()` | `inertiaStream()` |
86
+ | Criterion | `inertia()` | `inertiaStream()` |
87
87
  | -------------- | ---------------------- | ---------------------------------- |
88
88
  | Return type | `Promise<void>` | `Promise<void>` |
89
89
  | Rendering | Buffered HTML string | Streaming `renderToReadableStream` |
package/docs/lifecycle.md CHANGED
@@ -244,7 +244,7 @@ the pipeline sets `ctx.response`), and `onResponseSent()` (after the response is
244
244
  sent). The full phase reference lives in
245
245
  [The Application](/docs/application) and [Service Providers](/docs/providers).
246
246
 
247
- ```typescript
247
+ ```typescript fragment
248
248
  // app/providers/PaymentProvider.ts
249
249
  import { ServiceProvider } from "zerotal";
250
250
 
@@ -303,7 +303,7 @@ Boot and shutdown phases in order, and the per-request hook surface.
303
303
  `app.bootDurationMs` is the total. `app.providerReport` is the breakdown — one
304
304
  `ProviderReport` per provider, in boot order:
305
305
 
306
- ```typescript
306
+ ```typescript fragment
307
307
  for (const { name, durationMs, bindings } of app.providerReport) {
308
308
  console.log(`${name} ${durationMs}ms, bound ${bindings.length}`);
309
309
  }