@zerotal/inertia 1.7.4 → 1.8.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/CHANGELOG.md +14 -0
- package/api-surface.md +0 -18
- package/package.json +2 -2
- package/src/commands/InertiaBuildCommand.ts +21 -2
- package/src/middleware/InertiaMiddleware.ts +32 -11
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,20 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.8.0] — 2026-08-24
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **A `303` redirect was not marked as Inertia's, so the browser did nothing at all.**
|
|
16
|
+
`X-Inertia: true` was set inside the 302-to-303 conversion, so it only ever reached a
|
|
17
|
+
redirect arriving as a 301 or 302 from a non-GET handler. A handler returning the 303 the
|
|
18
|
+
protocol asks for skipped the only line that marked its response — and `redirect(to, 303)`
|
|
19
|
+
is exactly what `docs/authentication.md` tells people to write, in eight places. The form
|
|
20
|
+
submitted, the row was written, the mail went out, and the fields stayed filled in: a hang
|
|
21
|
+
from both ends. Marking now happens for every redirect status on an Inertia request, with
|
|
22
|
+
the conversion a separate decision on top of it. `307` and `308` are marked but left alone,
|
|
23
|
+
since preserving the method is the whole reason to choose them.
|
|
24
|
+
|
|
11
25
|
## [1.7.1] — 2026-08-16
|
|
12
26
|
|
|
13
27
|
### Fixed
|
package/api-surface.md
CHANGED
|
@@ -188,16 +188,10 @@ const inertiaStream = PageRenderer
|
|
|
188
188
|
|
|
189
189
|
const route = RouteBuilder
|
|
190
190
|
|
|
191
|
-
function _getHtmlTemplate = () => string
|
|
192
|
-
|
|
193
|
-
function _setHtmlTemplate = (html: string) => void
|
|
194
|
-
|
|
195
191
|
function always = { <T>(value: PropFactory<T>): AlwaysProp<T>; <T>(value: T): AlwaysProp<T>;}
|
|
196
192
|
|
|
197
193
|
function assetVersion = () => string
|
|
198
194
|
|
|
199
|
-
function buildPageObject = (component: string, props: Record<string, unknown>) => Promise<PageObject>
|
|
200
|
-
|
|
201
195
|
function clearHistory = () => void
|
|
202
196
|
|
|
203
197
|
function deepMerge = { <T>(value: PropFactory<T>): MergeProp<T>; <T>(value: T): MergeProp<T>;}
|
|
@@ -206,14 +200,10 @@ function defer = <T>(callback: PropFactory<T>, group?: string, options?: { re
|
|
|
206
200
|
|
|
207
201
|
function defineRoutes = (table: RouteTable) => void
|
|
208
202
|
|
|
209
|
-
function detectVuePlugin = (cwd: string) => Promise<BunPlugin[]>
|
|
210
|
-
|
|
211
203
|
function devtoolsEnabled = () => boolean
|
|
212
204
|
|
|
213
205
|
function encryptHistory = (on?: boolean) => void
|
|
214
206
|
|
|
215
|
-
function generatePageRegistry = (cwd?: string, pagesDir?: string) => Promise<void>
|
|
216
|
-
|
|
217
207
|
function hasRoute = (name: string) => boolean
|
|
218
208
|
|
|
219
209
|
function InertiaConfig = (options?: Partial<InertiaConfigShape>) => InertiaConfigShape
|
|
@@ -230,18 +220,10 @@ function optional = <T>(callback: PropFactory<T>) => OptionalProp<T>
|
|
|
230
220
|
|
|
231
221
|
function resetRoutes = () => void
|
|
232
222
|
|
|
233
|
-
function resolveProps = (raw: Record<string, unknown>, headers: Headers, component: string) => Promise<ResolvedPage>
|
|
234
|
-
|
|
235
223
|
function scroll = (value: PaginatorLike | PropFactory, options?: { pageName?: string; dataPath?: string;}) => InfiniteScrollProp
|
|
236
224
|
|
|
237
|
-
function setAssetVersion = (v: string) => void
|
|
238
|
-
|
|
239
|
-
function setHistoryEncryptionDefault = (on: boolean) => void
|
|
240
|
-
|
|
241
225
|
function share = { (key: string, value: unknown): void; (values: Record<string, unknown>): void;}
|
|
242
226
|
|
|
243
|
-
function sharedProps = () => Record<string, unknown>
|
|
244
|
-
|
|
245
227
|
interface DevtoolsEntry = {
|
|
246
228
|
__meta: { id: string; method: string; url: string; status: number; requestType: DevtoolsRequestType; component: string | null; timestamp: string; utime: number; tabUuid: string | null; batchId: string | null; serverTimingMs: number | null; redirectLocation?: string | null; visitId?: string | null;}
|
|
247
229
|
componentPath: string | null
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerotal/inertia",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"maturity": "stable",
|
|
6
6
|
"private": false,
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"typecheck": "tsc --noEmit"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@zerotal/core": "1.
|
|
36
|
+
"@zerotal/core": "1.8.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"react": "^18 || ^19",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Command } from "@zerotal/core";
|
|
2
|
-
import { pruneBuildOutput, browserEnvDefines } from "@zerotal/core/dev";
|
|
2
|
+
import { pruneBuildOutput, cleanBuildOutput, browserEnvDefines } from "@zerotal/core/dev";
|
|
3
3
|
import { generatePageRegistry } from "../PageRegistry.ts";
|
|
4
4
|
import { detectCssPlugins } from "../css.ts";
|
|
5
5
|
import { detectVuePlugin } from "../vuePlugin.ts";
|
|
@@ -32,6 +32,15 @@ export class InertiaBuildCommand extends Command {
|
|
|
32
32
|
description: "Build for production (minified, no source maps)",
|
|
33
33
|
default: false,
|
|
34
34
|
},
|
|
35
|
+
{
|
|
36
|
+
name: "clean",
|
|
37
|
+
short: "c",
|
|
38
|
+
type: "boolean" as const,
|
|
39
|
+
description:
|
|
40
|
+
"Delete everything in public/assets that this build did not write. " +
|
|
41
|
+
"Without it, only chunks and files the last build on THIS machine recorded are removed",
|
|
42
|
+
default: false,
|
|
43
|
+
},
|
|
35
44
|
];
|
|
36
45
|
|
|
37
46
|
override async run(): Promise<void> {
|
|
@@ -72,7 +81,17 @@ export class InertiaBuildCommand extends Command {
|
|
|
72
81
|
|
|
73
82
|
// Chunks are named after their content, so the ones this build replaced
|
|
74
83
|
// would otherwise stay behind — and ship.
|
|
75
|
-
|
|
84
|
+
//
|
|
85
|
+
// Which of the two runs matters most on a build machine that starts clean
|
|
86
|
+
// every time. The prune's record of the last build lives in `.zerotal/`,
|
|
87
|
+
// which is gitignored, so a fresh checkout has nothing to compare against
|
|
88
|
+
// and only chunk-named files are recognised. `--clean` needs no record: this
|
|
89
|
+
// directory belongs to the build, and what the build did not write does not
|
|
90
|
+
// belong in it.
|
|
91
|
+
const clean = this.flags["clean"] as boolean;
|
|
92
|
+
const removed = clean
|
|
93
|
+
? await cleanBuildOutput(outdir, result.outputs)
|
|
94
|
+
: await pruneBuildOutput(outdir, result.outputs);
|
|
76
95
|
|
|
77
96
|
this.info(`Build complete: ${result.outputs.length} files → public/assets/`);
|
|
78
97
|
if (removed.length > 0) this.dim(` Removed ${removed.length} stale file(s).`);
|
|
@@ -21,6 +21,15 @@ import { assetVersion } from "../version.ts";
|
|
|
21
21
|
* 3. Always set Vary: X-Inertia on all responses
|
|
22
22
|
* Ensures browser cache treats HTML and JSON versions as distinct.
|
|
23
23
|
*/
|
|
24
|
+
/**
|
|
25
|
+
* Every status the client can be handed as a redirect.
|
|
26
|
+
*
|
|
27
|
+
* 307 and 308 are here to be *marked*, not converted — an app that picks a
|
|
28
|
+
* method-preserving redirect means it, and a response the client cannot
|
|
29
|
+
* recognise as Inertia's is the failure this list exists to prevent.
|
|
30
|
+
*/
|
|
31
|
+
const REDIRECT_STATUSES = [301, 302, 303, 307, 308];
|
|
32
|
+
|
|
24
33
|
export class InertiaMiddleware extends BaseMiddleware {
|
|
25
34
|
protected options: Record<string, never> = {};
|
|
26
35
|
|
|
@@ -51,34 +60,46 @@ export class InertiaMiddleware extends BaseMiddleware {
|
|
|
51
60
|
|
|
52
61
|
const status = response.status;
|
|
53
62
|
const method = http.request.method;
|
|
54
|
-
const isRedirect =
|
|
63
|
+
const isRedirect = REDIRECT_STATUSES.includes(status);
|
|
55
64
|
|
|
56
|
-
// Fragment redirects: a redirect whose target carries a URL fragment (#...) on an Inertia
|
|
57
|
-
// request becomes a 409 + X-Inertia-Redirect, so the client performs a standard Inertia visit
|
|
58
|
-
// (preserving the fragment) instead of a full reload.
|
|
59
65
|
if (isInertia && isRedirect) {
|
|
60
66
|
const target = response.headers.get("Location") ?? "";
|
|
67
|
+
|
|
68
|
+
// Fragment redirects: a redirect whose target carries a URL fragment (#...) on an Inertia
|
|
69
|
+
// request becomes a 409 + X-Inertia-Redirect, so the client performs a standard Inertia visit
|
|
70
|
+
// (preserving the fragment) instead of a full reload.
|
|
61
71
|
if (target.includes("#")) {
|
|
62
|
-
// Same header-preservation concern as the
|
|
72
|
+
// Same header-preservation concern as the redirect branch below.
|
|
63
73
|
const headers = new Headers(response.headers);
|
|
64
74
|
headers.delete("Location");
|
|
65
75
|
headers.set("X-Inertia-Redirect", target);
|
|
66
76
|
headers.set("X-Inertia", "true");
|
|
67
77
|
return new Response(null, { status: 409, headers });
|
|
68
78
|
}
|
|
69
|
-
}
|
|
70
79
|
|
|
71
|
-
// Convert 302 to 303 for non-GET Inertia redirects
|
|
72
|
-
// Inertia requires 303 so browsers use GET on the redirect target
|
|
73
|
-
if (isInertia && [301, 302].includes(status) && method !== "GET") {
|
|
74
80
|
// Carry the original headers over. Rebuilding the Response from just `Location` dropped
|
|
75
81
|
// every other header the handler set — most importantly `Set-Cookie`, so `POST /login`
|
|
76
82
|
// returned a 303 to /dashboard with the session cookie discarded and the user still
|
|
77
83
|
// logged out.
|
|
78
84
|
const headers = new Headers(response.headers);
|
|
79
|
-
headers.set("Location",
|
|
85
|
+
headers.set("Location", target || "/");
|
|
86
|
+
|
|
87
|
+
// Stamped on *every* Inertia redirect, not only the ones converted below.
|
|
88
|
+
//
|
|
89
|
+
// It used to be set inside the conversion, which meant a handler that
|
|
90
|
+
// already returned the 303 the protocol asks for — `http.redirect(to, 303)`
|
|
91
|
+
// — skipped the only line that marked the response as Inertia's. The
|
|
92
|
+
// request succeeded, the row was written, and the form sat there with the
|
|
93
|
+
// fields still filled in: the worst shape a failure can take, because
|
|
94
|
+
// nothing about it looks like an error from either end.
|
|
80
95
|
headers.set("X-Inertia", "true");
|
|
81
|
-
|
|
96
|
+
if (!headers.has("Vary")) headers.set("Vary", "X-Inertia");
|
|
97
|
+
|
|
98
|
+
// A non-GET 301/302 becomes a 303, so the browser follows with GET instead
|
|
99
|
+
// of repeating the method against the target. 307 and 308 are left alone:
|
|
100
|
+
// preserving the method is the whole reason to choose them.
|
|
101
|
+
const needsSeeOther = method !== "GET" && (status === 301 || status === 302);
|
|
102
|
+
return new Response(null, { status: needsSeeOther ? 303 : status, headers });
|
|
82
103
|
}
|
|
83
104
|
|
|
84
105
|
// Never wrap streaming responses (e.g. SSE) — re-creating the Response
|