@stratal/inertia 0.0.27 → 0.1.1
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/CHANGELOG.md +379 -0
- package/README.md +158 -14
- package/dist/build-seo-tags-DBsHKxX9.mjs.map +1 -1
- package/dist/{decorate-B7nr7eBl.mjs → decorate-RQD1h28J.mjs} +1 -1
- package/dist/generator/type-generator.worker.d.mts +1 -1
- package/dist/generator/type-generator.worker.mjs +1 -1
- package/dist/index.d.mts +214 -92
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +390 -130
- package/dist/index.mjs.map +1 -1
- package/dist/quarry.d.mts +6 -8
- package/dist/quarry.d.mts.map +1 -1
- package/dist/quarry.mjs +127 -12
- package/dist/quarry.mjs.map +1 -1
- package/dist/react/access.d.mts +95 -0
- package/dist/react/access.d.mts.map +1 -0
- package/dist/react/access.mjs +133 -0
- package/dist/react/access.mjs.map +1 -0
- package/dist/react-dom-server-legacy-stub.d.mts +20 -0
- package/dist/react-dom-server-legacy-stub.d.mts.map +1 -0
- package/dist/react-dom-server-legacy-stub.mjs +25 -0
- package/dist/react-dom-server-legacy-stub.mjs.map +1 -0
- package/dist/react.d.mts +4 -6
- package/dist/react.d.mts.map +1 -1
- package/dist/react.mjs +1 -1
- package/dist/react.mjs.map +1 -1
- package/dist/seo-runtime.d.mts +1 -1
- package/dist/seo-runtime.mjs +8 -6
- package/dist/seo-runtime.mjs.map +1 -1
- package/dist/services/ssr-exclusion.d.mts +38 -0
- package/dist/services/ssr-exclusion.d.mts.map +1 -0
- package/dist/services/ssr-exclusion.mjs +0 -0
- package/dist/services/ssr-exclusion.mjs.map +1 -0
- package/dist/ssr.d.mts +37 -8
- package/dist/ssr.d.mts.map +1 -1
- package/dist/ssr.mjs +7 -4
- package/dist/ssr.mjs.map +1 -1
- package/dist/testing.d.mts +3 -2
- package/dist/testing.d.mts.map +1 -1
- package/dist/testing.mjs +20 -6
- package/dist/testing.mjs.map +1 -1
- package/dist/{type-generator-DFpha_Fp.mjs → type-generator-BVw8mj1y.mjs} +373 -64
- package/dist/type-generator-BVw8mj1y.mjs.map +1 -0
- package/dist/types-BltKoOR7.d.mts +193 -0
- package/dist/types-BltKoOR7.d.mts.map +1 -0
- package/dist/types-D-j_Ee_h.d.mts +52 -0
- package/dist/types-D-j_Ee_h.d.mts.map +1 -0
- package/dist/types-DzE1pdZs.d.mts.map +1 -1
- package/dist/vite.d.mts +19 -6
- package/dist/vite.d.mts.map +1 -1
- package/dist/vite.mjs +67 -5
- package/dist/vite.mjs.map +1 -1
- package/package.json +38 -27
- package/dist/type-generator-DFpha_Fp.mjs.map +0 -1
- package/dist/types-BhgXhWx6.d.mts +0 -82
- package/dist/types-BhgXhWx6.d.mts.map +0 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
# @stratal/inertia
|
|
2
|
+
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 99e36c5: Fix `ReferenceError: require is not defined` 500 on every Inertia SSR page under the Cloudflare Workers
|
|
8
|
+
dev/SSR runtime.
|
|
9
|
+
|
|
10
|
+
`@stratal/inertia`'s SSR renderer imports the `react-dom/server` subpath. Because `@stratal/inertia` is
|
|
11
|
+
listed in the `stratalInertia()` plugin's `optimizeDepsExclude` (to avoid the Hono/stratal
|
|
12
|
+
duplicate-instance identity bug), Vite's optimizer never crawls into it and so never auto-discovers
|
|
13
|
+
`react-dom/server`. React 19's `react-dom/server` is a CJS shim whose conditional
|
|
14
|
+
`require('./cjs/react-dom-server.<env>.<mode>.js')` is only resolved by the optimizer's CJS→ESM
|
|
15
|
+
conversion — left undiscovered, that `require` reaches the workerd SSR runner (which has no `require`),
|
|
16
|
+
so every SSR page 500s. Force-optimizing the exact `react-dom/server` specifier makes esbuild perform the
|
|
17
|
+
CJS→ESM conversion; the framework's import then redirects to the converted copy.
|
|
18
|
+
|
|
19
|
+
- Updated dependencies [df54f36]
|
|
20
|
+
- stratal@0.1.1
|
|
21
|
+
- @stratal/testing@0.1.1
|
|
22
|
+
|
|
23
|
+
## 0.1.0
|
|
24
|
+
|
|
25
|
+
### Minor Changes
|
|
26
|
+
|
|
27
|
+
- a753e55: Add build-time SSR exclusion, client-side access control and `ctx.scroll()` for infinite scroll, and make Inertia pages cacheable.
|
|
28
|
+
|
|
29
|
+
### Build-time SSR exclusion
|
|
30
|
+
- Add `ssrExclude` to the `stratalInertia()` Vite plugin. Client-only pages and their heavy dependencies were previously always bundled into the worker, because the SSR page glob pulled in every page; disabling SSR at runtime skipped rendering but still shipped the code.
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
stratalInertia({ ssrExclude: ["Admin/**", "Reports/Heavy"] });
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Patterns are matched against the page name, where `*` is a single segment and `**` any number. Excluded pages are dropped from the worker bundle and rendered client-only, while the browser bundle still includes them so they hydrate normally.
|
|
37
|
+
|
|
38
|
+
- Apply `ssrExclude` to an array-form page glob such as `import.meta.glob(['./pages/**/*.tsx', '!...'])`, keeping the negative patterns it already had. Only the single-string form worked before, so an array-form resolver silently kept every page in the worker bundle. A glob that cannot be rewritten now emits a build warning naming the file.
|
|
39
|
+
- Rewrite `import.meta.glob` resolvers that pass a second argument, such as `{ eager: true }`, preserving those options.
|
|
40
|
+
|
|
41
|
+
### Client-side access control
|
|
42
|
+
- Add the `<Can>`, `<Cannot>`, `<HasRole>` and `<HasNoRole>` components plus the `useCan`, `useRole` and `useAccess` hooks, on a new `@stratal/inertia/react/access` entry. They are gated on permissions the server shares automatically once `accessControl` is configured, and permission strings and role names are type-checked against a generated registry.
|
|
43
|
+
|
|
44
|
+
### Infinite scroll
|
|
45
|
+
|
|
46
|
+
Add **`ctx.scroll(callback, options?)`** for Inertia v3 infinite scroll, which makes `@inertiajs/react`'s `<InfiniteScroll>` work against a Stratal route. Until now the page carried no scroll metadata at all and the component threw before rendering.
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
return ctx.inertia("notes/Index", {
|
|
50
|
+
notes: ctx.scroll(() => this.service.paginate(page), { matchOn: "id" }),
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- The identifiers are derived; there is nothing to restate. Two shapes are recognised directly: the offset shape of `paginatedResponseSchema`, and `@stratal/framework`'s `db.$cursor` result. Any other shape throws `UnrecognizedScrollShapeError` rather than guessing, because a wrong next page reads to the client as "no more pages" and silently truncates the list. Pass `metadata` to name the identifiers for a third-party shape.
|
|
55
|
+
- The prop value keeps its paginator shape and only the rows under `wrapper` (default `data`) accumulate. Options are `wrapper`, `matchOn`, `pageName` and `metadata`.
|
|
56
|
+
- **`matchOn` has never deduplicated anything.** Entries were emitted in a form the client resolved to no prop, so every merge fell back to plain concatenation. A row that changes between two pages of a merged list is now collapsed instead of appearing twice.
|
|
57
|
+
- **`X-Inertia-Reset` is now honoured.** The header was parsed and discarded, so a prop the client named in it was joined to rather than replaced.
|
|
58
|
+
- Adds the `assertInertiaScrollProp(prop, expected?)` assertion and exports `UnrecognizedScrollShapeError` alongside the scroll option and metadata types.
|
|
59
|
+
|
|
60
|
+
### Caching
|
|
61
|
+
- Cache partial reloads, and with them every `ctx.defer()` prop, by declaring the Inertia protocol headers in `Vary` on every response. Deferred props are delivered by a follow-up partial reload, and those were refused outright, so a page that defers its expensive work kept all of that work uncached and caching bought close to nothing. Adds the `INERTIA_VARY_HEADERS` export naming the set. **`Vary` now lists these names on every Inertia response**, where it previously listed only `X-Inertia`, so anything asserting on that exact header value needs updating.
|
|
62
|
+
- Skip caching for pages that cannot be shared between callers: a page carrying flash data or a `once()` prop is not cached. On a cache hit the SSR render is skipped entirely, so a cached page costs no render.
|
|
63
|
+
- Narrow into a nested prop on a partial reload instead of answering with the whole of its parent. `only: ['auth.user']` asks for one field of `auth`; sending all of `auth` is the payload the partial reload was made to avoid. Prop metadata now names every entry by its full path, so a `defer()` nested under another prop is advertised where the client will look for it.
|
|
64
|
+
|
|
65
|
+
### Server rendering and dev runtime
|
|
66
|
+
- Add a **`prepare(page)`** hook to `createInertiaSsrApp`, which runs once per `render(page)` call and hands its result to `setup` as `prepared`. It exists so a request-scoped value can reach the tree without a module-level variable — a Workers isolate serves many requests concurrently, so module-level "current request" state is a cross-request leak. Omit it and `prepared` is `undefined`, which the type now enforces.
|
|
67
|
+
- Recycle the dev worker when its memory reaches a threshold, fixing frequent dev-server crashes in large apps. Under sustained HMR the dev isolate's heap grows until it hits the V8 limit and the worker aborts, which the browser shows as "Fetch failed". `quarry inertia:dev` now keeps the dev server alive, with a default threshold of 900 MB configurable through `--heap-limit=<MB>`. Supervision runs on macOS and Linux; elsewhere it is disabled with a warning.
|
|
68
|
+
- Strip react-dom's unused legacy synchronous server renderer from the worker SSR bundle, dropping around 197 KB raw from a minimal app. SSR is streaming-only, so `renderToString` and `renderToStaticMarkup` are not available in the worker.
|
|
69
|
+
- Fix every SSR page returning a 500 with `ReferenceError: require is not defined` or `module is not defined` under the Workers dev and SSR runtime. React 19's server entry, `react-dom/client`, the ORM data layer and the email renderer all reach CommonJS through packages excluded from Vite's optimizer, so their conditional `require` reached the worker runtime unconverted. An app that happened to import `react-dom` elsewhere was unaffected, while a minimal app failed on every request.
|
|
70
|
+
- Fix a guest SSR render failing at app init with `createPoolFactory is not a function` under a linked or portal checkout.
|
|
71
|
+
- Export `DocumentRendererService`, which renders a built `Page` into an HTML document and owns the single decision between streaming SSR and a client-only shell. `InertiaService` and `@stratal/inertia-modal` both delegate to it, so anything rendering an Inertia document outside those paths should inject the token rather than duplicate the branch.
|
|
72
|
+
|
|
73
|
+
### Fixes
|
|
74
|
+
- Answer a version mismatch with a real 409 instead of a 500. The mismatch branch set a status and headers but returned no response, so configuring `version` turned every stale client into a server error rather than the reload the check exists to trigger.
|
|
75
|
+
- Send the current asset version on that response, so the client can tell "this client is out of date" apart from an ordinary external redirect. The cancelable `location` event now reports `versionChange: true`, and async visits are left alone instead of reloading the page underneath a background request; both were previously unreachable.
|
|
76
|
+
- Reconcile the client head on a visit that only changed the props of the component already on screen. Closing a modal is exactly that shape, so the head previously kept the level's title while the address had moved back to the page's.
|
|
77
|
+
- Type the shared page props of an app that registers Inertia from a config namespace. `inertia:types` read `sharedData` and `accessControl` out of `src/app.module.ts` alone, and only as a literal, so an app composing its modules elsewhere or passing `config.asProvider()` had every shared prop reach pages as `{}` and access control never resolve. Both are now read wherever the registration lives, and a provider argument is followed back to the factory it came from.
|
|
78
|
+
- Pick up `ctx.modal()` calls in the type generator the same way as `ctx.inertia()`. If you hand-wrote prop types for a modal page, remove them and let the generated type be the only source.
|
|
79
|
+
- Fix two type-generator bugs that gave page props the wrong types: `ctx.share()` calls were not detected at all, and shared props wrapped in `always()`, `defer()`, `optional()`, `merge()` or `once()` were typed as the wrapper instead of the value it resolves to.
|
|
80
|
+
- Stop inlining the full i18n message-key union into page-prop types, which can shrink generated declaration files by an order of magnitude on apps with large key sets. Nullable and optional key unions no longer defeat detection, and props covering the full key set reference `MessageKeys` from `stratal/i18n`.
|
|
81
|
+
- Add `SeoService.contributed()`, which reports whether anything has called `ctx.seo()` on this request — what a caller rendering one page over another needs in order to keep the underlying page's metadata instead of overwriting it with the defaults.
|
|
82
|
+
- Add `InertiaService.resolveProps()` and `partialRequestFor()`, so a caller assembling its own page can resolve props with the same semantics `render()` applies.
|
|
83
|
+
|
|
84
|
+
### Breaking Changes
|
|
85
|
+
- **`ssr.disabled` is removed** from `InertiaModule.forRoot({ ssr })`. Replace it with the Vite plugin's `ssrExclude`, which both skips SSR and drops the excluded pages from the worker bundle: `stratalInertia({ ssrExclude: ['Admin/**'] })`.
|
|
86
|
+
- **`ctx.withoutSsr()` and the `withoutSsr` context variable are removed.** SSR exclusion is now build-time and declarative, so there is no per-request runtime opt-out.
|
|
87
|
+
- **`Vary` now lists every Inertia protocol header on every response**, not just `X-Inertia`. Update anything asserting on that exact value.
|
|
88
|
+
- **The validation API is `zod/mini`.** The `z` re-export is gone from the validation surface this package re-exports. Import schema builders directly from `zod/mini` using named imports and replace classic chaining with the functional API: `z.string().min(1).optional()` becomes `optional(string().check(minLength(1)))`.
|
|
89
|
+
- **OpenAPI documents are generated lazily**, on the first request to the docs endpoint. `OpenAPIService.getSpec()` becomes `getSpec(container)` and is async, and `routeFilter` is now a metadata predicate `(route: RouteSchemaMeta) => boolean` instead of `(path, pathItem)`.
|
|
90
|
+
|
|
91
|
+
### Patch Changes
|
|
92
|
+
|
|
93
|
+
- Updated dependencies [a753e55]
|
|
94
|
+
- Updated dependencies [a753e55]
|
|
95
|
+
- stratal@0.1.0
|
|
96
|
+
- @stratal/testing@0.1.0
|
|
97
|
+
|
|
98
|
+
## 0.0.27
|
|
99
|
+
|
|
100
|
+
### Patch Changes
|
|
101
|
+
|
|
102
|
+
- Updated dependencies [41a9140]
|
|
103
|
+
- stratal@0.0.27
|
|
104
|
+
- @stratal/testing@0.0.27
|
|
105
|
+
|
|
106
|
+
## 0.0.26
|
|
107
|
+
|
|
108
|
+
### Patch Changes
|
|
109
|
+
|
|
110
|
+
- ab95f52: Fix flash cookie encoding crashing on non-Latin1 characters
|
|
111
|
+
|
|
112
|
+
### Details
|
|
113
|
+
- Flash cookies are now encoded with UTF-8-safe base64 — `btoa` alone threw on any character outside Latin1 (em-dashes, smart quotes, non-Latin scripts), which are routine in user-facing flash messages
|
|
114
|
+
|
|
115
|
+
- bb6d3b9: Trailing-slash exclusions: `trailingSlash` accepts `{ mode, exclude }`
|
|
116
|
+
|
|
117
|
+
### Details
|
|
118
|
+
- `trailingSlash` application config now accepts `{ mode, exclude }` alongside a bare mode. Excluded paths are never redirected (308) and never rewritten by URL generation — for routes whose canonical form is owned externally (e.g. OAuth redirect URIs matched byte-for-byte).
|
|
119
|
+
- String patterns are segment-aware prefixes; RegExp patterns match both slash forms of the pathname regardless of anchoring.
|
|
120
|
+
- Exclusions match in route space: with path-based locale detection, a leading locale segment is stripped before matching, so `'/callback'` also exempts `/fr/callback` — in the redirect middleware, `Uri` helpers, and hreflang link generation.
|
|
121
|
+
- `@stratal/inertia` threads the widened config through hreflang URL generation and shares only the resolved mode with the React client (exclusions are server-side; excluded paths are served in both slash forms, so client-built URLs never redirect).
|
|
122
|
+
- New exports from `stratal/router`: `resolveTrailingSlash`, `isTrailingSlashExcluded`, and the `TrailingSlashConfig` / `TrailingSlashOptions` / `TrailingSlashExclude` types.
|
|
123
|
+
|
|
124
|
+
- Updated dependencies [ab95f52]
|
|
125
|
+
- Updated dependencies [ab95f52]
|
|
126
|
+
- Updated dependencies [bb6d3b9]
|
|
127
|
+
- stratal@0.0.26
|
|
128
|
+
- @stratal/testing@0.0.26
|
|
129
|
+
|
|
130
|
+
## 0.0.25
|
|
131
|
+
|
|
132
|
+
### Patch Changes
|
|
133
|
+
|
|
134
|
+
- e93db60: Add `--inspector-port` option to `inertia:dev` for configuring the worker debugger inspector port
|
|
135
|
+
|
|
136
|
+
Set a distinct port per worker to avoid `EADDRINUSE` when running multiple Inertia workers concurrently, or pass `false` to disable the inspector entirely.
|
|
137
|
+
|
|
138
|
+
- Updated dependencies [e93db60]
|
|
139
|
+
- stratal@0.0.25
|
|
140
|
+
- @stratal/testing@0.0.25
|
|
141
|
+
|
|
142
|
+
## 0.0.24
|
|
143
|
+
|
|
144
|
+
### Patch Changes
|
|
145
|
+
|
|
146
|
+
- 10cf223: Stream server-side rendering with React 19 for faster TTFB and progressive Suspense rendering
|
|
147
|
+
|
|
148
|
+
The document shell (SEO + CSS) now flushes immediately while the app body streams, and `React.lazy`/`Suspense` boundaries stream in progressively instead of blocking the whole response. A new `createInertiaSsrApp` helper from `@stratal/inertia/ssr` wires this up for you. `quarry inertia:install` scaffolds an `src/inertia/ssr.tsx` using it.
|
|
149
|
+
|
|
150
|
+
`createInertiaSsrApp` is generic over your page props — call `createInertiaSsrApp<MyProps>({ … })` to type the resolver, or omit the type argument to keep the `import.meta.glob` resolver opaque (the default). A downstream cancellation (client disconnect) now propagates to the React render, and an invalid resolver result throws instead of rendering nothing.
|
|
151
|
+
|
|
152
|
+
Also fixes `ssr.disabled` glob matching, which previously compared against the full URL and so missed routes carrying a query string (e.g. `admin/*` vs `/admin/dashboard?tab=users`); it now matches the pathname only. Rerunning `quarry inertia:install` on an existing install now wires the SSR bundle into the current `InertiaModule.forRoot({ … })` instead of leaving SSR silently disabled.
|
|
153
|
+
|
|
154
|
+
### Breaking Changes
|
|
155
|
+
- The SSR bundle now returns a stream, and there is no longer a silent client-side fallback — if SSR fails to load or render, the error surfaces (500) instead of degrading silently.
|
|
156
|
+
- Migrate your `src/inertia/ssr.tsx` to use the new helper:
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
import { createInertiaSsrApp } from "@stratal/inertia/ssr";
|
|
160
|
+
|
|
161
|
+
export const { render } = createInertiaSsrApp({
|
|
162
|
+
resolve: async (name) => {
|
|
163
|
+
const pages = import.meta.glob("./pages/**/*.tsx");
|
|
164
|
+
const page = await pages[`./pages/${name}.tsx`]?.();
|
|
165
|
+
if (!page) throw new Error(`Page not found: ${name}`);
|
|
166
|
+
return page;
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Replace the previous `createInertiaApp` + `renderToString` setup, which returned `{ head, body }`. App-level providers go in the optional `setup` callback. Document metadata should come from server-side `ctx.seo()` — a `<Head>` inside a suspended boundary is not captured during streaming.
|
|
172
|
+
|
|
173
|
+
- stratal@0.0.24
|
|
174
|
+
- @stratal/testing@0.0.24
|
|
175
|
+
|
|
176
|
+
## 0.0.23
|
|
177
|
+
|
|
178
|
+
### Patch Changes
|
|
179
|
+
|
|
180
|
+
- 13b0e8d: Add `@stratal/feature-flags` — Cloudflare Flagship feature flags via the native Worker binding API.
|
|
181
|
+
- `FeatureFlagModule.forRoot({ apps: [{ binding, flags }], default, context })` with a declare-once flag manifest, manifest defaults, a per-request evaluation-context resolver, and multi-app support via `FeatureFlagService.use(binding)`.
|
|
182
|
+
- `FeatureFlagShareMiddleware` shares evaluated flags to Inertia pages as the `featureFlags` prop; register it yourself (scoped to page controllers via `router.middleware(...)` or app-wide via `router.use(...)`) so a stalled Flagship binding can't block unrelated routes. Typed `useFlag` / `useFeatureFlags` hooks on `@stratal/feature-flags/react`. No runtime dependency on `@stratal/inertia`.
|
|
183
|
+
- `@stratal/inertia`: expose a generic `ctx.share(key, value)` macro on `RouterContext` so middleware and packages can contribute per-request shared props.
|
|
184
|
+
- `@stratal/framework`: add a `ctx.user()` macro on `RouterContext` (shorthand for `AuthContext.requireUser()`).
|
|
185
|
+
|
|
186
|
+
- 13b0e8d: Fix correctness and security issues found in review.
|
|
187
|
+
|
|
188
|
+
Queue:
|
|
189
|
+
- Retry the correct binding: dispatch stamps the producer binding into message metadata and failed jobs record it, so `queue:retry` re-enqueues through the Cloudflare binding instead of the queue name (which is not a valid binding key and broke retry whenever the two differed). A message with no binding metadata is logged and acked rather than stored as an unretryable job.
|
|
190
|
+
- Honor the documented retry budget: `maxRetries` now counts retries correctly against Cloudflare's 1-based `message.attempts` (previously gave one fewer retry than configured).
|
|
191
|
+
- Derive idempotency keys from an order-stable serialization of `type` + `payload`, so payloads that differ only in key order dedupe correctly.
|
|
192
|
+
- `queue:retry --all` / `queue:purge --all --queue` collect matching keys before deleting, so cursor pagination no longer skips jobs; `queue:failed --queue --limit` now counts matching jobs rather than scanned keys.
|
|
193
|
+
- Documented that delivery is at-least-once with best-effort de-duplication (not exactly-once), since the processed marker is written only after a handler succeeds and KV is eventually consistent — handlers must be idempotent.
|
|
194
|
+
|
|
195
|
+
Email (SMTP):
|
|
196
|
+
- Upgrade STARTTLS onto the socket `startTls()` returns: the original socket is closed by the runtime, so the post-upgrade reader/writer are re-derived from the new secure socket and any pre-handshake bytes are discarded (fixes a broken `smtp://` STARTTLS path on real Workers and closes the STARTTLS plaintext-injection vector).
|
|
197
|
+
- Refuse to send credentials over an unencrypted connection: an `smtp://` server that doesn't offer STARTTLS now fails loudly instead of leaking the password (blocks STARTTLS-stripping downgrades). Credential-free connections (e.g. local Mailpit) are unaffected.
|
|
198
|
+
- AUTH is gated on the server's advertised mechanisms and supports both `PLAIN` and `LOGIN`; usernames are percent-decoded like passwords.
|
|
199
|
+
- Add a response timeout so a hung SMTP server can't wedge the worker; QUIT/socket close are now best-effort and never mask a successful send.
|
|
200
|
+
- MIME builder strips CR/LF from headers, escapes/RFC 2231-encodes attachment filenames (prevents header injection), base64-encodes message bodies (fixes long-line corruption), and rejects envelope addresses containing whitespace or angle brackets (prevents `MAIL FROM`/`RCPT TO` desync).
|
|
201
|
+
|
|
202
|
+
Inertia SEO:
|
|
203
|
+
- `titleTemplate` substitutes every `%s` and treats `$`-sequences in the title literally.
|
|
204
|
+
- Inject head/body content via function replacements, so SEO/page content containing `$`-sequences (`$$`, `$&`, `` $` ``, `$'`) is no longer corrupted or able to splice a template placeholder back into the output.
|
|
205
|
+
- Drop unsafe attribute names — including inline event handlers (`on*`) — from custom `meta`/`link` entries (prevents tag breakout server-side, `setAttribute` errors during client head-sync, and developer-supplied event-handler attributes).
|
|
206
|
+
|
|
207
|
+
Feature flags:
|
|
208
|
+
- `FeatureFlagService.use()` binds the target app exactly once.
|
|
209
|
+
|
|
210
|
+
Database (framework):
|
|
211
|
+
- The reentrant `$transaction` proxy forwards the receiver for non-transaction property access.
|
|
212
|
+
|
|
213
|
+
Testing:
|
|
214
|
+
- `TestingModule.close()` drops the isolated per-file database even if shutdown throws; the stale-database sweep escapes LIKE metacharacters so a prefix containing `_` can't over-match.
|
|
215
|
+
|
|
216
|
+
DI:
|
|
217
|
+
- Construct singletons against the root container so they can never capture a request-scoped dependency (which would leak one request's state across every later request); an illegal singleton→request dependency now throws loudly.
|
|
218
|
+
- Detect circular dependencies and throw a clear error naming the cycle instead of overflowing the stack.
|
|
219
|
+
- `tryResolve` only swallows "no provider"; a registered provider that throws while constructing now surfaces the real error instead of injecting `undefined`.
|
|
220
|
+
- Request-cache invalidation tracks transitive constructor dependencies, so re-registering a value rebuilds cached services that depend on it through a transient intermediary.
|
|
221
|
+
|
|
222
|
+
Quarry dev runtime:
|
|
223
|
+
- Persist every durable plugin (KV, D1, R2, Durable Objects, cache) under `.wrangler/state/v3`, matching `wrangler dev` (previously only R2 was persisted); load `.env.local` / `.env.<env>.local` into `process.env` for full parity.
|
|
224
|
+
- The `cloudflare:sockets` STARTTLS shim re-attaches the stream error handler to the upgraded socket, so post-upgrade connection errors still surface.
|
|
225
|
+
|
|
226
|
+
- 13b0e8d: Add backend-driven SEO metadata management with hreflang and automatic client-side head synchronization
|
|
227
|
+
- Configure app-wide SEO defaults and a title template via `InertiaModule.forRoot({ seo: { ... } })`.
|
|
228
|
+
- Set per-page metadata from controllers or middleware with `ctx.seo({ ... })` — title, description, Open Graph, Twitter card, canonical URL, and arbitrary meta/link tags.
|
|
229
|
+
- Locale alternates (`rel="alternate" hreflang="…"`) are generated automatically for path-prefixed and querystring locale strategies and merged into the rendered tags.
|
|
230
|
+
- Server-rendered SEO tags are kept in sync with the document head across SPA navigations automatically — no app wiring required.
|
|
231
|
+
- New `useSeo()` React hook to read the resolved SEO data in components.
|
|
232
|
+
- New `@stratal/inertia/seo` entry point exporting SEO types and tag-building utilities.
|
|
233
|
+
- Fix: error responses for idempotent GET/HEAD navigations (e.g. deferred partial reloads) now render in place instead of using flash + redirect, preventing redirect loops.
|
|
234
|
+
|
|
235
|
+
- Updated dependencies [13b0e8d]
|
|
236
|
+
- Updated dependencies [13b0e8d]
|
|
237
|
+
- Updated dependencies [13b0e8d]
|
|
238
|
+
- Updated dependencies [13b0e8d]
|
|
239
|
+
- Updated dependencies [13b0e8d]
|
|
240
|
+
- Updated dependencies [13b0e8d]
|
|
241
|
+
- Updated dependencies [13b0e8d]
|
|
242
|
+
- Updated dependencies [13b0e8d]
|
|
243
|
+
- Updated dependencies [13b0e8d]
|
|
244
|
+
- Updated dependencies [13b0e8d]
|
|
245
|
+
- Updated dependencies [13b0e8d]
|
|
246
|
+
- Updated dependencies [be813bc]
|
|
247
|
+
- Updated dependencies [be813bc]
|
|
248
|
+
- @stratal/testing@0.0.23
|
|
249
|
+
- stratal@0.0.23
|
|
250
|
+
|
|
251
|
+
## 0.0.22
|
|
252
|
+
|
|
253
|
+
### Patch Changes
|
|
254
|
+
|
|
255
|
+
- 1658945: Add `createClientViteConfig` helper, client manifest injection, sourcemap option, and `InertiaQuarryModule` for CLI integration
|
|
256
|
+
- New `createClientViteConfig()` produces a ready-made Vite config for the client bundle with automatic reflect-metadata invocation for tsyringe compatibility.
|
|
257
|
+
- Inertia build command now injects the client manifest into the SSR bundle for asset resolution.
|
|
258
|
+
- Type generator enhanced to extract controller page prop types with promise unwrapping.
|
|
259
|
+
- New `@stratal/inertia/quarry` export provides `InertiaQuarryModule` for registering Inertia CLI commands.
|
|
260
|
+
|
|
261
|
+
- 4b273ea: Replace @intlify/core-base with intl-messageformat in `useI18n` hook, add eager deferred prop resolution, and remove tsyringe/reflect-metadata dependencies
|
|
262
|
+
- `useI18n()` now uses `intl-messageformat` for ICU message formatting. The hook API is unchanged.
|
|
263
|
+
- New `x-inertia-resolve-deferred` request header causes all deferred props to be resolved eagerly in the response, skipping client-side lazy loading.
|
|
264
|
+
- The `invokeReflectMetadataBeforeTsyringeCheck` Vite plugin is removed (no longer needed).
|
|
265
|
+
- `reflect-metadata` and `@intlify/core-base` are no longer peer dependencies.
|
|
266
|
+
|
|
267
|
+
- Updated dependencies [1658945]
|
|
268
|
+
- Updated dependencies [1658945]
|
|
269
|
+
- Updated dependencies [4b273ea]
|
|
270
|
+
- Updated dependencies [4b273ea]
|
|
271
|
+
- @stratal/testing@0.0.22
|
|
272
|
+
- stratal@0.0.22
|
|
273
|
+
|
|
274
|
+
## 0.0.21
|
|
275
|
+
|
|
276
|
+
### Patch Changes
|
|
277
|
+
|
|
278
|
+
- 3489cfd: Dedupe React and Inertia in the Vite resolver to prevent duplicate-copy bugs
|
|
279
|
+
|
|
280
|
+
`stratalInertia()` now adds the React ecosystem (`react`, `react-dom`, `react-is`, `scheduler`, `use-sync-external-store`) and `@inertiajs/core` / `@inertiajs/react` to `resolve.dedupe` and `resolve.noExternal`. React 19's main entry is CJS and must run through the optimizer, but when Vite re-runs optimization after auto-discovering a new dep it would mint a second `?v=<hash>` copy, breaking React identity (`Invalid hook call`, dispatcher mismatch). Forcing a single physical copy through `dedupe`/`noExternal` keeps hooks, contexts, and Inertia internals working across re-optimizations.
|
|
281
|
+
|
|
282
|
+
- 3489cfd: Run Inertia type generation in a worker thread and cache dev CSS per HMR cycle
|
|
283
|
+
- The Vite types plugin now offloads `runTypeGeneration` to a debounced (250ms) worker via `node:worker_threads`, so HMR no longer blocks on ts-morph parsing. A second edit while a worker is in flight queues exactly one follow-up run, and the dispatcher is torn down on `closeBundle`.
|
|
284
|
+
- `writeInertiaTypes` skips the write when the on-disk content already matches and otherwise writes via a temp-file rename, so the file is never observed half-written.
|
|
285
|
+
- `stratalInertiaDevCss` caches the collected SSR CSS and invalidates it on CSS-module HMR, eliminating duplicate scans when the SSR endpoint and the virtual module are both requested.
|
|
286
|
+
- Component names with `-`, `_`, or whitespace now PascalCase correctly when forming `<Name>PageProps` (e.g. `user-profile/edit` → `UserProfileEditPageProps`).
|
|
287
|
+
|
|
288
|
+
- Updated dependencies [3489cfd]
|
|
289
|
+
- Updated dependencies [3489cfd]
|
|
290
|
+
- Updated dependencies [3489cfd]
|
|
291
|
+
- stratal@0.0.21
|
|
292
|
+
- @stratal/testing@0.0.21
|
|
293
|
+
|
|
294
|
+
## 0.0.20
|
|
295
|
+
|
|
296
|
+
### Patch Changes
|
|
297
|
+
|
|
298
|
+
- f8c61e1: Expose the matched route on `useRoute()` and apply trailing-slash + sticky params
|
|
299
|
+
|
|
300
|
+
The `routes` Inertia shared prop now also carries a `route` snapshot for the current request (`{ name, params, defaults }`) and the application's `trailingSlash` mode, enabling several `useRoute()` enhancements:
|
|
301
|
+
- `currentRoute` is returned alongside `route` and `current`, so components can read the matched route name and params directly (e.g. `currentRoute.params.id`).
|
|
302
|
+
- `current(name)` now accepts dotted wildcard patterns derived from real route names (e.g. `current('users.*')`), strictly typed against `StratalRouteMap`.
|
|
303
|
+
- `route(name, params)` merges sticky defaults from `Uri.defaults()` and any current-route params declared by the target route, so values like `tenantId` carry over without the caller passing them. Explicit params still win.
|
|
304
|
+
- Generated URLs respect the server's `trailingSlash` mode.
|
|
305
|
+
- Catch-all path params (e.g. `:slug{.+}`) preserve forward slashes when encoded, matching the server-side behavior.
|
|
306
|
+
|
|
307
|
+
Also exports `resolveUrl`, `matchCurrent`, and `applyTrailingSlash` as pure helpers for non-React callers and tests.
|
|
308
|
+
|
|
309
|
+
- f8c61e1: Skip response mutation for non-cloneable status codes
|
|
310
|
+
|
|
311
|
+
The Inertia middleware would crash with a `RangeError` when the downstream handler returned a response whose status fell outside `200-599` (e.g. WebSocket upgrades using `101`, or `Response.error()`'s status `0`), because adding the `Vary` header forces Hono to re-construct the `Response` and the constructor rejects those statuses. The middleware now passes such responses through untouched. The `302 → 303` rewrite for non-GET/HEAD Inertia requests is now scoped to only run when the status is exactly `302`.
|
|
312
|
+
|
|
313
|
+
- f8c61e1: Loosen peer dependency ranges for broader compatibility
|
|
314
|
+
|
|
315
|
+
Peer dependencies (`@inertiajs/*`, `hono`, `react`, `react-dom`, `vite`, `vitest`, `@intlify/core-base`, `reflect-metadata`, `stratal`) now use `>=` ranges instead of pinned `^` ranges, so apps can adopt newer majors of these packages without waiting for a coordinated bump.
|
|
316
|
+
|
|
317
|
+
- f8c61e1: Exclude `hono`, `stratal`, and Hono OpenAPI plugins from Vite pre-bundling
|
|
318
|
+
|
|
319
|
+
`stratalInertia()` now adds `stratal`, `hono`, `@hono/zod-openapi`, and `@hono/swagger-ui` to `optimizeDeps.exclude`. Pre-bundling those packages produced duplicate copies in `.vite/deps_<env>/`, so Response objects from one instance flowed into a Hono Context from the other and crashed inside the `set res` setter (`this.#res.headers.entries is not a function`). Excluding them keeps a single shared instance.
|
|
320
|
+
|
|
321
|
+
- Updated dependencies [f8c61e1]
|
|
322
|
+
- Updated dependencies [f8c61e1]
|
|
323
|
+
- Updated dependencies [f8c61e1]
|
|
324
|
+
- Updated dependencies [f8c61e1]
|
|
325
|
+
- Updated dependencies [f8c61e1]
|
|
326
|
+
- stratal@0.0.20
|
|
327
|
+
- @stratal/testing@0.0.20
|
|
328
|
+
|
|
329
|
+
## 0.0.19
|
|
330
|
+
|
|
331
|
+
### Patch Changes
|
|
332
|
+
|
|
333
|
+
- 5d26c24: Add `--persist-to` option to `inertia:dev` for shared emulator state
|
|
334
|
+
|
|
335
|
+
The `inertia:dev` command now accepts a `--persist-to=<dir>` flag that is forwarded to `@cloudflare/vite-plugin` as `persistState.path`. This lets multiple workers running in development share the same R2, KV, and cache emulator state.
|
|
336
|
+
|
|
337
|
+
- Updated dependencies [3b16f5b]
|
|
338
|
+
- Updated dependencies [5d26c24]
|
|
339
|
+
- Updated dependencies [3b16f5b]
|
|
340
|
+
- Updated dependencies [3b16f5b]
|
|
341
|
+
- Updated dependencies [3b16f5b]
|
|
342
|
+
- Updated dependencies [5d26c24]
|
|
343
|
+
- Updated dependencies [5d26c24]
|
|
344
|
+
- Updated dependencies [3b16f5b]
|
|
345
|
+
- stratal@0.0.19
|
|
346
|
+
- @stratal/testing@0.0.19
|
|
347
|
+
|
|
348
|
+
## 0.0.18
|
|
349
|
+
|
|
350
|
+
### Patch Changes
|
|
351
|
+
|
|
352
|
+
- c9176ea: Add precognition support, i18n integration, flash messages, React hooks, and testing utilities
|
|
353
|
+
|
|
354
|
+
### Details
|
|
355
|
+
- Add precognition middleware for form validation without full submission
|
|
356
|
+
- Add i18n integration with automatic locale and translation sharing to Inertia pages
|
|
357
|
+
- Add flash message support via cookie-based flash store
|
|
358
|
+
- Add `useRoute` and `useI18n` React hooks (`@stratal/inertia/react`)
|
|
359
|
+
- Add `@stratal/inertia/testing` subpath with TestResponse assertion augments for Inertia responses
|
|
360
|
+
- Enhance Vite configuration with Cloudflare Vite plugin support
|
|
361
|
+
|
|
362
|
+
- 17f8675: Add Inertia.js v3 server adapter for building server-driven React SPAs with Stratal
|
|
363
|
+
|
|
364
|
+
### Details
|
|
365
|
+
- `InertiaModule` with `forRoot()` / `forRootAsync()` configuration
|
|
366
|
+
- `InertiaService` for rendering pages with shared data, deferred props, and partial reload support
|
|
367
|
+
- `@InertiaRoute()` decorator for Inertia-specific controller routes
|
|
368
|
+
- Inertia middleware for handling `X-Inertia` protocol (version checking, 409 conflict responses)
|
|
369
|
+
- Vite integration with dev CSS injection and automatic type generation plugins
|
|
370
|
+
- SSR rendering support via `@inertiajs/react/server`
|
|
371
|
+
- Quarry CLI commands: `inertia:dev`, `inertia:build`, `inertia:install`, `inertia:types`
|
|
372
|
+
|
|
373
|
+
- Updated dependencies [fcb71c4]
|
|
374
|
+
- Updated dependencies [17f8675]
|
|
375
|
+
- Updated dependencies [c9176ea]
|
|
376
|
+
- Updated dependencies [c9176ea]
|
|
377
|
+
- Updated dependencies [c9176ea]
|
|
378
|
+
- stratal@0.0.18
|
|
379
|
+
- @stratal/testing@0.0.18
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @stratal/inertia
|
|
2
2
|
|
|
3
|
-
Inertia.js v3 server adapter for [Stratal](https://
|
|
3
|
+
Inertia.js v3 server adapter for [Stratal](https://stratal.dev) — build server-driven React SPAs on Cloudflare Workers.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@stratal/inertia)
|
|
6
6
|
[](https://github.com/strataljs/stratal/actions/workflows/ci.yml)
|
|
@@ -16,11 +16,17 @@ Inertia.js v3 server adapter for [Stratal](https://github.com/strataljs/stratal)
|
|
|
16
16
|
## Features
|
|
17
17
|
|
|
18
18
|
- **InertiaModule** — Drop-in Stratal module with `forRoot()` / `forRootAsync()` configuration
|
|
19
|
-
- **Streaming SSR** — React 19 `renderToReadableStream` streaming via `createInertiaSsrApp
|
|
20
|
-
- **Shared Data** — Global shared props with static values or request-scoped resolvers
|
|
21
|
-
-
|
|
22
|
-
- **Partial Reloads** —
|
|
23
|
-
- **
|
|
19
|
+
- **Streaming SSR** — React 19 `renderToReadableStream` streaming via `createInertiaSsrApp`
|
|
20
|
+
- **Shared Data** — Global shared props with static values or request-scoped resolvers, plus `ctx.share()` from middleware
|
|
21
|
+
- **Route Decorators** — `@InertiaRoute` and `@InertiaGet` / `@InertiaPost` / `@InertiaPut` / `@InertiaPatch` / `@InertiaDelete`
|
|
22
|
+
- **Partial Reloads** — `defer`, `optional`, `merge`, `once`, `always` and `scroll` props
|
|
23
|
+
- **Backend-driven SEO** — `ctx.seo()` with app-wide defaults and a title template; tags are injected into `<head>` and kept in sync across client navigations
|
|
24
|
+
- **i18n Sharing** — Auto-share backend messages as `locale` + `translations`, read with `useI18n()`
|
|
25
|
+
- **Named Routes** — Serialize named routes to the client for Ziggy-like URL generation with `useRoute()`
|
|
26
|
+
- **Flash Messages** — `ctx.flash()` with a pluggable store (cookie store included)
|
|
27
|
+
- **Vite Plugin** — `stratalInertia()` handles dev/build wiring, asset manifests, and SSR page exclusion
|
|
28
|
+
- **Quarry CLI** — `inertia:install`, `inertia:dev`, `inertia:build`, and `inertia:types`
|
|
29
|
+
- **Test Assertions** — `assertInertia()` and friends via `@stratal/inertia/testing`
|
|
24
30
|
|
|
25
31
|
## Installation
|
|
26
32
|
|
|
@@ -30,6 +36,12 @@ npm install @stratal/inertia
|
|
|
30
36
|
yarn add @stratal/inertia
|
|
31
37
|
```
|
|
32
38
|
|
|
39
|
+
Then scaffold the frontend:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx quarry inertia:install
|
|
43
|
+
```
|
|
44
|
+
|
|
33
45
|
### AI Agent Skills
|
|
34
46
|
|
|
35
47
|
Stratal provides [Agent Skills](https://agentskills.io) for AI coding assistants like Claude Code and Cursor. Install to give your AI agent knowledge of Stratal patterns, conventions, and APIs:
|
|
@@ -40,7 +52,7 @@ npx skills add strataljs/stratal
|
|
|
40
52
|
|
|
41
53
|
| Skill | Description |
|
|
42
54
|
|---|---|
|
|
43
|
-
| `stratal` | Build Cloudflare Workers apps with the Stratal framework — modules, DI, controllers, routing, OpenAPI, queues, cron, events, seeders, CLI, auth, database,
|
|
55
|
+
| `stratal` | Build Cloudflare Workers apps with the Stratal framework — modules, DI, controllers, routing, OpenAPI, queues, cron, events, seeders, CLI, auth, database, access control, testing, and more |
|
|
44
56
|
|
|
45
57
|
## Quick Start
|
|
46
58
|
|
|
@@ -67,6 +79,19 @@ class AppModule {}
|
|
|
67
79
|
export default new Stratal({ module: AppModule })
|
|
68
80
|
```
|
|
69
81
|
|
|
82
|
+
`rootView` is the only required option. The rest are optional: `version`, `ssr`, `flash`, `sharedData`, `i18n`, `routes`, `seo` and `entryClientPath` (defaults to `src/inertia/app.tsx`).
|
|
83
|
+
|
|
84
|
+
### Vite setup
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// vite.config.ts
|
|
88
|
+
import { stratalInertia } from '@stratal/inertia/vite'
|
|
89
|
+
|
|
90
|
+
export default defineConfig({
|
|
91
|
+
plugins: [stratalInertia()],
|
|
92
|
+
})
|
|
93
|
+
```
|
|
94
|
+
|
|
70
95
|
### Controller with @InertiaRoute
|
|
71
96
|
|
|
72
97
|
```typescript
|
|
@@ -82,7 +107,85 @@ export class NotesController {
|
|
|
82
107
|
}
|
|
83
108
|
```
|
|
84
109
|
|
|
85
|
-
|
|
110
|
+
## Props
|
|
111
|
+
|
|
112
|
+
`InertiaModule` augments `RouterContext` with prop helpers that control what is sent and when:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
async index(ctx: RouterContext) {
|
|
116
|
+
return ctx.inertia('notes/Index', {
|
|
117
|
+
// Sent on every response
|
|
118
|
+
notes: await this.notes.all(),
|
|
119
|
+
|
|
120
|
+
// Resolved after the initial render, optionally in a named group
|
|
121
|
+
stats: ctx.defer(() => this.notes.stats(), 'sidebar'),
|
|
122
|
+
|
|
123
|
+
// Only when the client explicitly asks for it
|
|
124
|
+
audit: ctx.optional(() => this.notes.audit()),
|
|
125
|
+
|
|
126
|
+
// Merged into existing client-side data instead of replacing it
|
|
127
|
+
feed: ctx.merge(() => this.notes.page(), { matchOn: 'id' }),
|
|
128
|
+
|
|
129
|
+
// Sent once, then cached by the client
|
|
130
|
+
countries: ctx.once(() => this.geo.countries()),
|
|
131
|
+
|
|
132
|
+
// Always evaluated, even on a partial reload
|
|
133
|
+
unread: ctx.always(() => this.notes.unreadCount()),
|
|
134
|
+
|
|
135
|
+
// A merge prop that also publishes what <InfiniteScroll> needs
|
|
136
|
+
items: ctx.scroll(() => this.notes.paginate()),
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`ctx.share(key, value)` adds a shared prop for the current request — useful from middleware — and `ctx.flash(key, value)` sets flash data for the next visit.
|
|
142
|
+
|
|
143
|
+
## SEO
|
|
144
|
+
|
|
145
|
+
Set `seo` on the module for app-wide defaults, then contribute per-page metadata from a controller. The resolved tags are injected into `<head>`, shared as the `seo` prop, and kept in sync across client navigations by the runtime the Vite plugin injects.
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
InertiaModule.forRoot({
|
|
149
|
+
rootView: 'app',
|
|
150
|
+
seo: {
|
|
151
|
+
defaults: { openGraph: { siteName: 'Acme' }, twitter: { card: 'summary_large_image' } },
|
|
152
|
+
titleTemplate: '%s — Acme',
|
|
153
|
+
},
|
|
154
|
+
})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
async show(ctx: RouterContext) {
|
|
159
|
+
const note = await this.notes.find(ctx.param('id'))
|
|
160
|
+
ctx.seo({ title: note.title, description: note.excerpt })
|
|
161
|
+
return ctx.inertia('notes/Show', { note })
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Read it in a component with `useSeo()` from `@stratal/inertia/react`.
|
|
166
|
+
|
|
167
|
+
## i18n and named routes
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
InertiaModule.forRoot({
|
|
171
|
+
rootView: 'app',
|
|
172
|
+
i18n: { only: ['common', 'nav'] }, // shares `locale` + `translations`
|
|
173
|
+
routes: true, // shares named routes
|
|
174
|
+
})
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
import { useI18n, useRoute } from '@stratal/inertia/react'
|
|
179
|
+
|
|
180
|
+
const { t } = useI18n()
|
|
181
|
+
const { route, current } = useRoute()
|
|
182
|
+
|
|
183
|
+
<a href={route('notes.show', { id })} aria-current={current('notes.show') ? 'page' : undefined}>
|
|
184
|
+
{t('common.view')}
|
|
185
|
+
</a>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Streaming SSR
|
|
86
189
|
|
|
87
190
|
Enable SSR by pointing the module at a bundle that exports a streaming `render`:
|
|
88
191
|
|
|
@@ -93,9 +196,7 @@ InertiaModule.forRoot({
|
|
|
93
196
|
})
|
|
94
197
|
```
|
|
95
198
|
|
|
96
|
-
`src/inertia/ssr.tsx` (scaffolded by `quarry inertia:install`) uses
|
|
97
|
-
`createInertiaSsrApp`, which wires Inertia's `App`, head collection, and React 19's
|
|
98
|
-
`renderToReadableStream` — the shell flushes early and the body streams progressively:
|
|
199
|
+
`src/inertia/ssr.tsx` (scaffolded by `quarry inertia:install`) uses `createInertiaSsrApp`, which wires Inertia's `App`, head collection, and React 19's `renderToReadableStream` — the shell flushes early and the body streams progressively:
|
|
99
200
|
|
|
100
201
|
```tsx
|
|
101
202
|
import { createInertiaSsrApp } from '@stratal/inertia/ssr'
|
|
@@ -110,14 +211,57 @@ export const { render } = createInertiaSsrApp({
|
|
|
110
211
|
})
|
|
111
212
|
```
|
|
112
213
|
|
|
113
|
-
There is no client-side fallback — an SSR failure surfaces as an error rather than
|
|
114
|
-
|
|
115
|
-
|
|
214
|
+
There is no client-side fallback — an SSR failure surfaces as an error rather than silently degrading.
|
|
215
|
+
|
|
216
|
+
### Excluding pages from SSR
|
|
217
|
+
|
|
218
|
+
Heavy pages that don't need to be in first paint can skip the server entirely. Pass `ssrExclude` to the Vite plugin with page-component globs:
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
stratalInertia({
|
|
222
|
+
ssrExclude: ['Admin/**', 'Reports/Heavy'],
|
|
223
|
+
})
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Patterns match Inertia component names (`*` matches one path segment, `**` matches any number). Excluded pages are dropped from the worker bundle entirely — a smaller cold start — and rendered client-only at runtime. The browser bundle still includes them, so they hydrate normally.
|
|
227
|
+
|
|
228
|
+
## Testing
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
// vitest.setup.ts
|
|
232
|
+
import '@stratal/inertia/testing' // augments TestResponse with Inertia assertions
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
const response = await module.http
|
|
237
|
+
.get('/notes')
|
|
238
|
+
.withHeaders({ 'X-Inertia': 'true', 'X-Inertia-Version': '1' })
|
|
239
|
+
.send()
|
|
240
|
+
|
|
241
|
+
await response.assertInertia()
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Quarry commands
|
|
245
|
+
|
|
246
|
+
| Command | Description |
|
|
247
|
+
|---|---|
|
|
248
|
+
| `inertia:install` | Scaffold Inertia.js files for a Stratal project |
|
|
249
|
+
| `inertia:dev` | Start the Inertia Vite development server |
|
|
250
|
+
| `inertia:build` | Build the Inertia frontend for production |
|
|
251
|
+
| `inertia:types` | Generate Inertia page type definitions |
|
|
116
252
|
|
|
117
253
|
## Documentation
|
|
118
254
|
|
|
119
255
|
Full guides and examples are available at **[stratal.dev](https://stratal.dev)**.
|
|
120
256
|
|
|
257
|
+
## Support the project
|
|
258
|
+
|
|
259
|
+
If Stratal is useful to you, **[star the repository](https://github.com/strataljs/stratal)** — it is the simplest way to help others find it.
|
|
260
|
+
|
|
261
|
+
## Maintainer
|
|
262
|
+
|
|
263
|
+
Built and maintained by **Temitayo Fadojutimi** — [@adesege_](https://x.com/adesege_).
|
|
264
|
+
|
|
121
265
|
## License
|
|
122
266
|
|
|
123
267
|
MIT
|