@zerotal/inertia 1.7.5 → 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
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/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
|