@blacklizard/inertia-express 3.0.0 → 3.1.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.
package/README.md CHANGED
@@ -18,9 +18,10 @@ The internal core is framework-agnostic, so future adapters for Fastify, Hono, N
18
18
  - `res.inertia(component, props, options)` Express helper
19
19
  - Shared props (static or per-request)
20
20
  - Optional, deferred (with rescue), always, merge, deep-merge, once, and infinite-scroll props
21
+ - Live props (`liveProps` channel listeners) and `broadcastProps` payloads for your realtime server
21
22
  - Partial reloads (`X-Inertia-Partial-Data` / `X-Inertia-Partial-Except`)
22
23
  - Asset version handling and automatic 409 + `X-Inertia-Location` on mismatch
23
- - Inertia-aware redirects (auto 303 after PUT/PATCH/DELETE)
24
+ - Inertia-aware redirects (auto 303 after PUT/PATCH/DELETE/QUERY, 409 + `X-Inertia-Redirect` for `#fragment` targets, empty 200 → redirect back)
24
25
  - External redirects via `res.inertiaLocation()`
25
26
  - Validation error helpers
26
27
  - Pluggable root view
@@ -88,7 +89,7 @@ Pass options to `inertia({ ... })`:
88
89
  | `ssr` | `SsrRenderer` | SSR hook returning `{ head, body }`. See [SSR](#server-side-rendering-ssr). |
89
90
  | `cache` | `InertiaCacheOptions` | View cache for SSR HTML. See [View cache](#view-cache). |
90
91
  | `edgeCache` | `EdgeCachePolicy` | CDN `Cache-Control` policy. See [Edge caching](#edge-caching-cdn). |
91
- | `flashFromSession` | `boolean` | Auto-promote `req.session.errors` / `flash` into shared props. Default off. |
92
+ | `flashFromSession` | `boolean` | Surface `req.session.flash` as the top-level `flash` page key. Default off. |
92
93
 
93
94
  If no `rootView` is given, the middleware emits a minimal default HTML shell with an empty `<div id="app">` plus the v3 `<script type="application/json" data-page="app">` page tag, so you can wire up a Vite client manually.
94
95
 
@@ -121,11 +122,12 @@ inertia({
121
122
  auth: { user: req.user ?? null },
122
123
  csrf: req.csrfToken?.(),
123
124
  flash: req.session?.flash ?? {},
124
- errors: req.session?.errors ?? {},
125
125
  }),
126
126
  });
127
127
  ```
128
128
 
129
+ Validation errors do not need wiring here: `errors` is shared on every page automatically (see [Validation errors](#validation-errors)).
130
+
129
131
  ## Prop helpers
130
132
 
131
133
  Every prop value is either a plain value, a bare closure (evaluated on each
@@ -134,7 +136,7 @@ built by one of the constructors below. Descriptors are chainable and mirror
134
136
  inertia-laravel's prop classes; the resolver is a port of its `PropsResolver`.
135
137
 
136
138
  ```ts
137
- import { always, deepMerge, defer, merge, once, optional, scroll } from "@blacklizard/inertia-express";
139
+ import { always, deepMerge, defer, live, merge, once, optional, scroll } from "@blacklizard/inertia-express";
138
140
  ```
139
141
 
140
142
  A bare closure replaces 2.x's `lazy()`:
@@ -260,6 +262,41 @@ app.get("/posts", async (req, res) => {
260
262
 
261
263
  Laravel: `Inertia::scroll()`
262
264
 
265
+ ### `live`
266
+
267
+ A prop the client refreshes when a realtime event arrives. `live(value, { on,
268
+ channel, throttle? })` resolves like a plain prop and lists its channel
269
+ listeners in the page object's `liveProps`; `always`, `optional`, `defer` and
270
+ `once` descriptors accept the same options through `.live(options)`. Merge and
271
+ scroll props cannot be live. Events are strings and channels are explicit: a
272
+ bare string channel is public, and `{ name, type }` sets `private`,
273
+ `presence` or `encrypted-private`. `throttle` is in milliseconds.
274
+
275
+ `broadcastProps(props, onRescue?)` resolves every prop (no partial or once
276
+ filtering, rescued props omitted) into the `{ __inertia: { props } }` payload
277
+ to send with the event; `resolveBroadcastProps` returns the bare props map. The
278
+ adapter does not open sockets: send the payload with your own realtime server.
279
+ `req.inertia.socketId` carries the `X-Socket-Id` header (`null` when absent) so
280
+ that server can skip the sender.
281
+
282
+ ```ts
283
+ import { broadcastProps, live } from "@blacklizard/inertia-express";
284
+
285
+ app.get("/leads", async (req, res) => {
286
+ await res.inertia("Leads/Index", {
287
+ leads: live(() => loadLeads(), { on: "LeadUpdated", channel: { name: "leads", type: "private" } }),
288
+ });
289
+ });
290
+
291
+ // After a lead changes (Socket.IO shown)
292
+ // The client subscribes to the declared event on the declared channel.
293
+ io.to("leads")
294
+ .except(req.inertia?.socketId ?? [])
295
+ .emit("LeadUpdated", await broadcastProps({ leads: () => loadLeads() }));
296
+ ```
297
+
298
+ Laravel: `Inertia::live()` / `->live()`
299
+
263
300
  ### Rescue hook and fragment preservation
264
301
 
265
302
  Pass `onRescue: (error, path, req) => …` to `inertia({ … })` to report errors
@@ -302,18 +339,10 @@ The middleware applies this filtering automatically. Partial reload filtering is
302
339
 
303
340
  ## Validation errors
304
341
 
305
- The simplest pattern is to share `req.session.errors` from `sharedProps`:
306
-
307
- ```ts
308
- inertia({
309
- sharedProps: (req) => ({ errors: req.session?.errors ?? {} }),
310
- });
311
- ```
342
+ Record validation errors with `res.inertiaErrors(errors, bag?)`. Errors are stored per bag (`default` when no bag is given); a repeated bag replaces the earlier one and other bags are kept.
312
343
 
313
- The middleware also exposes `res.inertiaErrors(errors, bag?)`, which:
314
-
315
- - Writes errors to `req.session.errors` if a session is present.
316
- - Otherwise stores them on `res.locals.inertiaErrors` for the same response.
344
+ - With a session, it writes `req.session.inertia.errors[bag] = errors`; the next render reads them and clears them (read-once).
345
+ - Without a session, it writes the same bags map to `res.locals.inertiaErrors` for the current response.
317
346
 
318
347
  ```ts
319
348
  app.post("/users", (req, res) => {
@@ -326,11 +355,22 @@ app.post("/users", (req, res) => {
326
355
  });
327
356
  ```
328
357
 
329
- You may pass an error bag name to namespace errors under `errors[bag]`.
358
+ The `errors` prop is shared on every page (`{}` when empty, listed in `sharedProps`, kept on partial reloads) whether or not `flashFromSession` is on, so no `sharedProps` wiring is needed. Bags are remapped when the page is read, as in inertia-laravel's `Middleware::resolveValidationErrors`:
359
+
360
+ - Errors stored without a bag come back flat: `props.errors.email`.
361
+ - If the request carries `X-Inertia-Error-Bag: signup`, those same errors come back as `props.errors.signup.email`.
362
+ - Errors stored under a named bag (`res.inertiaErrors(errors, "createUser")`) come back keyed by name, `props.errors.createUser.email`, when no `default` bag is stored. If a `default` bag is stored as well, only `default` is returned (flat, or under the header name) and the named bags are dropped.
363
+
364
+ An explicit `errors` key in your shared or page props wins over the shared one.
330
365
 
331
366
  ## Redirects
332
367
 
333
- For Inertia requests, `res.redirect(url)` after a `PUT`, `PATCH`, or `DELETE` is automatically promoted to status `303`. This is required by the Inertia protocol to ensure the follow-up request is a `GET`.
368
+ For Inertia requests, a `302` from `res.redirect(url)` after a `PUT`, `PATCH`, `DELETE` or `QUERY` is automatically promoted to status `303`. This is required by the Inertia protocol to ensure the follow-up request is a `GET`.
369
+
370
+ Two more inertia-laravel middleware rules apply to Inertia requests:
371
+
372
+ - **Fragment redirects** (`Middleware::onRedirectWithFragment`) — a redirect whose URL contains `#` is answered with `409`, `X-Inertia-Redirect: <url>` (encoded like `Location`) and an empty body, so the client can navigate and keep the fragment. Prefetch requests get the normal redirect.
373
+ - **Empty responses** (`Middleware::onEmptyResponse`) — a `200` with an empty body (`res.end()`, `res.send("")`) becomes a redirect back to the `Referer`, or `/` without one (`303` after PUT/PATCH/DELETE/QUERY, `302` otherwise). Responses whose headers were already sent (`res.flushHeaders()`, streaming) and handlers that never respond are not covered.
334
374
 
335
375
  For external redirects (different host or full reload), use `res.inertiaLocation()`:
336
376
 
@@ -342,7 +382,7 @@ For Inertia (XHR) visits this returns `409 X-Inertia-Location: <url>` which the
342
382
 
343
383
  ## Asset versioning
344
384
 
345
- Pass a `version` resolver. When a client's `X-Inertia-Version` header doesn't match the current value on a `GET`, the middleware responds with `409` + `X-Inertia-Location: <same-url>`, and the Inertia client performs a full reload.
385
+ Pass a `version` resolver. When a client's `X-Inertia-Version` header doesn't match the current value on a `GET`, the middleware responds with `409` + `X-Inertia-Location: <same-url>` and `X-Inertia-Version: <current version>` (inertia-laravel's `Middleware::onVersionChange`), and the Inertia client performs a full reload.
346
386
 
347
387
  ```ts
348
388
  inertia({
@@ -557,13 +597,13 @@ Failures on individual routes never halt the run — inspect `summary.results` t
557
597
 
558
598
  ## Flash from session
559
599
 
560
- Set `flashFromSession: true` to auto-promote `req.session.errors` and `req.session.flash` into shared props (and clear them after read), so you don't have to wire them into `sharedProps` manually:
600
+ Set `flashFromSession: true` to surface `req.session.flash` as the top-level `flash` page key (a sibling of `props`, read on the client via `usePage().flash`) and clear it after read:
561
601
 
562
602
  ```ts
563
603
  inertia({ flashFromSession: true });
564
604
  ```
565
605
 
566
- Off by default — most apps already wire flash/errors via `sharedProps`. Requires a session middleware that exposes `req.session`.
606
+ Off by default. Requires a session middleware that exposes `req.session`. The option gates only `flash`; validation errors are shared as the `errors` prop on every page regardless (see [Validation errors](#validation-errors)).
567
607
 
568
608
  ## Edge caching (CDN)
569
609
 
@@ -1,2 +1,2 @@
1
- import { $ as OnceMetadata, A as leadsToOnly, B as renderErrorPage, C as once, D as PropKind, E as PropDescriptor, F as createPage, G as unpackDotProps, H as parseInertiaRequest, I as DefaultRootViewInput, J as SsrCacheStore, K as ComputeCacheKeyInput, L as ErrorPageInput, M as matchesOnly, N as pathIsSameOrDescendant, O as PropSource, P as CreatePageInput, Q as InertiaRequestInfo, R as encodePageScript, S as merge, T as scroll, U as ValidationErrors, V as INERTIA_HEADERS, W as scopeErrors, X as computeSsrCacheKey, Y as canonicalStringify, Z as InertiaPage, _ as ScrollProp, a as INERTIA_LOCATION_STATUS, at as SharedPropsInput, b as defer, c as PropsResolver, d as resolveProps, et as PageProps, f as AlwaysProp, g as OptionalProp, h as OnceProp, i as buildPage, it as ScrollMetadata, j as matchesExcept, k as ScrollOptions, l as ResolvePropsInput, m as MergeProp, n as resolveVersion, nt as RenderOptions, o as INERTIA_REDIRECT_STATUS, ot as VersionResolver, p as DeferProp, q as SsrCacheEntry, r as BuildPageInput, rt as RescueHandler, s as inertiaRedirectStatus, t as isVersionMismatch, tt as PropsMetadata, u as ResolvePropsResult, v as always, w as optional, x as isPropDescriptor, y as deepMerge, z as renderDefaultHtml } from "../index-PWaujyMo.js";
2
- export { AlwaysProp, BuildPageInput, ComputeCacheKeyInput, CreatePageInput, DefaultRootViewInput, DeferProp, ErrorPageInput, INERTIA_HEADERS, INERTIA_LOCATION_STATUS, INERTIA_REDIRECT_STATUS, type InertiaPage, type InertiaRequestInfo, MergeProp, type OnceMetadata, OnceProp, OptionalProp, type PageProps, PropDescriptor, type PropKind, type PropSource, type PropsMetadata, PropsResolver, type RenderOptions, type RescueHandler, ResolvePropsInput, ResolvePropsResult, type ScrollMetadata, type ScrollOptions, ScrollProp, type SharedPropsInput, SsrCacheEntry, SsrCacheStore, ValidationErrors, type VersionResolver, always, buildPage, canonicalStringify, computeSsrCacheKey, createPage, deepMerge, defer, encodePageScript, inertiaRedirectStatus, isPropDescriptor, isVersionMismatch, leadsToOnly, matchesExcept, matchesOnly, merge, once, optional, parseInertiaRequest, pathIsSameOrDescendant, renderDefaultHtml, renderErrorPage, resolveProps, resolveVersion, scopeErrors, scroll, unpackDotProps };
1
+ import { $ as ComputeCacheKeyInput, A as PropDescriptor, B as DefaultRootViewInput, C as live, D as scroll, E as optional, F as matchesExcept, G as INERTIA_HEADERS, H as encodePageScript, I as matchesOnly, J as ValidationErrorBags, K as PREFETCH_HEADERS, L as pathIsSameOrDescendant, M as PropSource, N as ScrollOptions, O as LiveChannelInput, P as leadsToOnly, Q as unpackDotProps, R as CreatePageInput, S as isPropDescriptor, T as once, U as renderDefaultHtml, V as ErrorPageInput, W as renderErrorPage, X as resolveValidationErrors, Y as ValidationErrors, Z as scopeErrors, _ as OptionalProp, _t as ScrollMetadata, a as INERTIA_LOCATION_STATUS, at as resolveBroadcastProps, b as deepMerge, c as PropsResolver, ct as LiveChannel, d as resolveProps, dt as LiveMetadata, et as SsrCacheEntry, f as AlwaysProp, ft as OnceMetadata, g as OnceProp, gt as RescueHandler, h as MergeProp, ht as RenderOptions, i as buildPage, it as broadcastProps, j as PropKind, k as LiveOptions, l as ResolvePropsInput, lt as LiveChannelType, m as LiveProp, mt as PropsMetadata, n as resolveVersion, nt as canonicalStringify, o as INERTIA_REDIRECT_STATUS, ot as InertiaPage, p as DeferProp, pt as PageProps, q as parseInertiaRequest, r as BuildPageInput, rt as computeSsrCacheKey, s as inertiaRedirectStatus, st as InertiaRequestInfo, t as isVersionMismatch, tt as SsrCacheStore, u as ResolvePropsResult, ut as LiveListener, v as ScrollProp, vt as SharedPropsInput, w as merge, x as defer, y as always, yt as VersionResolver, z as createPage } from "../index-C7vBpEDY.js";
2
+ export { AlwaysProp, BuildPageInput, ComputeCacheKeyInput, CreatePageInput, DefaultRootViewInput, DeferProp, ErrorPageInput, INERTIA_HEADERS, INERTIA_LOCATION_STATUS, INERTIA_REDIRECT_STATUS, type InertiaPage, type InertiaRequestInfo, type LiveChannel, type LiveChannelInput, type LiveChannelType, type LiveListener, type LiveMetadata, type LiveOptions, LiveProp, MergeProp, type OnceMetadata, OnceProp, OptionalProp, PREFETCH_HEADERS, type PageProps, PropDescriptor, type PropKind, type PropSource, type PropsMetadata, PropsResolver, type RenderOptions, type RescueHandler, ResolvePropsInput, ResolvePropsResult, type ScrollMetadata, type ScrollOptions, ScrollProp, type SharedPropsInput, SsrCacheEntry, SsrCacheStore, ValidationErrorBags, ValidationErrors, type VersionResolver, always, broadcastProps, buildPage, canonicalStringify, computeSsrCacheKey, createPage, deepMerge, defer, encodePageScript, inertiaRedirectStatus, isPropDescriptor, isVersionMismatch, leadsToOnly, live, matchesExcept, matchesOnly, merge, once, optional, parseInertiaRequest, pathIsSameOrDescendant, renderDefaultHtml, renderErrorPage, resolveBroadcastProps, resolveProps, resolveValidationErrors, resolveVersion, scopeErrors, scroll, unpackDotProps };
@@ -1,2 +1,2 @@
1
- import { A as canonicalStringify, C as defer, D as optional, E as once, O as scroll, S as deepMerge, T as merge, _ as INERTIA_HEADERS, a as INERTIA_REDIRECT_STATUS, b as unpackDotProps, c as resolveProps, d as matchesOnly, f as pathIsSameOrDescendant, g as renderErrorPage, h as renderDefaultHtml, i as INERTIA_LOCATION_STATUS, j as computeSsrCacheKey, k as PropDescriptor, l as leadsToOnly, m as encodePageScript, n as resolveVersion, o as inertiaRedirectStatus, p as createPage, r as buildPage, s as PropsResolver, t as isVersionMismatch, u as matchesExcept, v as parseInertiaRequest, w as isPropDescriptor, x as always, y as scopeErrors } from "../core-ChQbBw-k.js";
2
- export { INERTIA_HEADERS, INERTIA_LOCATION_STATUS, INERTIA_REDIRECT_STATUS, PropDescriptor, PropsResolver, always, buildPage, canonicalStringify, computeSsrCacheKey, createPage, deepMerge, defer, encodePageScript, inertiaRedirectStatus, isPropDescriptor, isVersionMismatch, leadsToOnly, matchesExcept, matchesOnly, merge, once, optional, parseInertiaRequest, pathIsSameOrDescendant, renderDefaultHtml, renderErrorPage, resolveProps, resolveVersion, scopeErrors, scroll, unpackDotProps };
1
+ import { A as isPropDescriptor, C as matchesExcept, D as always, E as unpackDotProps, F as scroll, I as PropDescriptor, M as merge, N as once, O as deepMerge, P as optional, S as leadsToOnly, T as pathIsSameOrDescendant, _ as computeSsrCacheKey, a as INERTIA_REDIRECT_STATUS, b as PropsResolver, c as encodePageScript, d as INERTIA_HEADERS, f as PREFETCH_HEADERS, g as canonicalStringify, h as scopeErrors, i as INERTIA_LOCATION_STATUS, j as live, k as defer, l as renderDefaultHtml, m as resolveValidationErrors, n as resolveVersion, o as inertiaRedirectStatus, p as parseInertiaRequest, r as buildPage, s as createPage, t as isVersionMismatch, u as renderErrorPage, v as broadcastProps, w as matchesOnly, x as resolveProps, y as resolveBroadcastProps } from "../core-PN_yVSCS.js";
2
+ export { INERTIA_HEADERS, INERTIA_LOCATION_STATUS, INERTIA_REDIRECT_STATUS, PREFETCH_HEADERS, PropDescriptor, PropsResolver, always, broadcastProps, buildPage, canonicalStringify, computeSsrCacheKey, createPage, deepMerge, defer, encodePageScript, inertiaRedirectStatus, isPropDescriptor, isVersionMismatch, leadsToOnly, live, matchesExcept, matchesOnly, merge, once, optional, parseInertiaRequest, pathIsSameOrDescendant, renderDefaultHtml, renderErrorPage, resolveBroadcastProps, resolveProps, resolveValidationErrors, resolveVersion, scopeErrors, scroll, unpackDotProps };