@blacklizard/inertia-express 2.0.0 → 3.0.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 +153 -23
- package/dist/core/index.d.ts +2 -552
- package/dist/core/index.js +2 -662
- package/dist/core-ChQbBw-k.js +1499 -0
- package/dist/core-ChQbBw-k.js.map +1 -0
- package/dist/express/index.d.ts +17 -3
- package/dist/express/index.d.ts.map +1 -1
- package/dist/express/index.js +35 -6
- package/dist/express/index.js.map +1 -1
- package/dist/index-PWaujyMo.d.ts +1133 -0
- package/dist/index-PWaujyMo.d.ts.map +1 -0
- package/package.json +2 -2
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/index.js.map +0 -1
package/dist/core/index.js
CHANGED
|
@@ -1,662 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Recursively normalize a value so equal-by-value inputs serialize identically:
|
|
5
|
-
* object keys are sorted, `undefined` properties are dropped, and non-finite
|
|
6
|
-
* numbers collapse to `null`.
|
|
7
|
-
*
|
|
8
|
-
* @param value Arbitrary JSON-shaped input.
|
|
9
|
-
*/
|
|
10
|
-
function canonicalize(value) {
|
|
11
|
-
if (value === null) return null;
|
|
12
|
-
if (typeof value !== "object") {
|
|
13
|
-
if (typeof value === "number" && !Number.isFinite(value)) return null;
|
|
14
|
-
return value;
|
|
15
|
-
}
|
|
16
|
-
if (Array.isArray(value)) return value.map(canonicalize);
|
|
17
|
-
const obj = value;
|
|
18
|
-
return Object.fromEntries(Object.keys(obj).sort().filter((key) => obj[key] !== void 0).map((key) => [key, canonicalize(obj[key])]));
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Stable JSON encoding: keys sorted recursively, undefined dropped, NaN/Inf
|
|
22
|
-
* normalized. Two equal-by-value objects always produce the same string,
|
|
23
|
-
* so they hash to the same cache key.
|
|
24
|
-
*
|
|
25
|
-
* @param value Arbitrary JSON-shaped input.
|
|
26
|
-
*/
|
|
27
|
-
function canonicalStringify(value) {
|
|
28
|
-
return JSON.stringify(canonicalize(value));
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Build the cache key for an SSR cache entry.
|
|
32
|
-
*
|
|
33
|
-
* Shape: `${prefix}:${version}:${component}:${sha256(props[+discriminator])}`
|
|
34
|
-
*
|
|
35
|
-
* Including `version` in the key namespace means a deploy that bumps the
|
|
36
|
-
* asset version atomically retires every old key — no flush, no race.
|
|
37
|
-
*
|
|
38
|
-
* @param input Prefix, version, component, resolved props, optional discriminator.
|
|
39
|
-
*/
|
|
40
|
-
function computeSsrCacheKey(input) {
|
|
41
|
-
const versionPart = input.version ?? "_";
|
|
42
|
-
const propsHash = createHash("sha256").update(canonicalStringify(input.props)).update(input.discriminator ?? "").digest("hex").slice(0, 32);
|
|
43
|
-
return `${input.prefix}:${versionPart}:${input.component}:${propsHash}`;
|
|
44
|
-
}
|
|
45
|
-
//#endregion
|
|
46
|
-
//#region src/core/errors.ts
|
|
47
|
-
/**
|
|
48
|
-
* Apply error-bag scoping to a flat errors object, matching the official
|
|
49
|
-
* Inertia behavior. When `bag` is set, errors are namespaced under
|
|
50
|
-
* `props.errors[bag]`. When `bag` is null, errors live at `props.errors`.
|
|
51
|
-
*
|
|
52
|
-
* @param errors Flat field-to-message map.
|
|
53
|
-
* @param bag Optional error-bag name (e.g. `"createUser"`); null for top-level.
|
|
54
|
-
*/
|
|
55
|
-
function scopeErrors(errors, bag) {
|
|
56
|
-
if (bag) return { errors: { [bag]: errors } };
|
|
57
|
-
return { errors };
|
|
58
|
-
}
|
|
59
|
-
//#endregion
|
|
60
|
-
//#region src/core/headers.ts
|
|
61
|
-
/**
|
|
62
|
-
* Canonical Inertia v3 protocol header names.
|
|
63
|
-
*
|
|
64
|
-
* The Inertia client sends these headers on every visit. The server uses
|
|
65
|
-
* them to decide between a full HTML response and a JSON page response,
|
|
66
|
-
* and to scope partial reloads.
|
|
67
|
-
*/
|
|
68
|
-
const INERTIA_HEADERS = {
|
|
69
|
-
inertia: "x-inertia",
|
|
70
|
-
version: "x-inertia-version",
|
|
71
|
-
partialComponent: "x-inertia-partial-component",
|
|
72
|
-
partialData: "x-inertia-partial-data",
|
|
73
|
-
partialExcept: "x-inertia-partial-except",
|
|
74
|
-
errorBag: "x-inertia-error-bag",
|
|
75
|
-
reset: "x-inertia-reset",
|
|
76
|
-
location: "x-inertia-location"
|
|
77
|
-
};
|
|
78
|
-
/**
|
|
79
|
-
* Read a header value as a single string, looking up the lowercase variant as
|
|
80
|
-
* a fallback. Returns the first entry of an array value and `null` when the
|
|
81
|
-
* header is absent.
|
|
82
|
-
*
|
|
83
|
-
* @param headers Raw header record from the HTTP layer.
|
|
84
|
-
* @param name Header name to read.
|
|
85
|
-
*/
|
|
86
|
-
function readHeader(headers, name) {
|
|
87
|
-
const value = headers[name] ?? headers[name.toLowerCase()];
|
|
88
|
-
if (value === void 0 || value === null) return null;
|
|
89
|
-
if (Array.isArray(value)) return value[0] ?? null;
|
|
90
|
-
return value;
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Split a comma-separated header value into a trimmed, non-empty string list.
|
|
94
|
-
* Returns `null` when the input is null or yields no entries.
|
|
95
|
-
*
|
|
96
|
-
* @param value Raw header value, or null when absent.
|
|
97
|
-
*/
|
|
98
|
-
function parseList(value) {
|
|
99
|
-
if (value === null) return null;
|
|
100
|
-
const parts = value.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
|
|
101
|
-
return parts.length > 0 ? parts : null;
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Parse a request's headers into a normalized {@link InertiaRequestInfo}.
|
|
105
|
-
*
|
|
106
|
-
* @param input Headers, HTTP method, and request URL. Method is uppercased in
|
|
107
|
-
* the returned object; the URL is passed through unchanged.
|
|
108
|
-
*/
|
|
109
|
-
function parseInertiaRequest(input) {
|
|
110
|
-
const { headers } = input;
|
|
111
|
-
const inertia = readHeader(headers, INERTIA_HEADERS.inertia);
|
|
112
|
-
const version = readHeader(headers, INERTIA_HEADERS.version);
|
|
113
|
-
const partialComponent = readHeader(headers, INERTIA_HEADERS.partialComponent);
|
|
114
|
-
const partialOnly = parseList(readHeader(headers, INERTIA_HEADERS.partialData));
|
|
115
|
-
const partialExcept = parseList(readHeader(headers, INERTIA_HEADERS.partialExcept));
|
|
116
|
-
const errorBag = readHeader(headers, INERTIA_HEADERS.errorBag);
|
|
117
|
-
const resetKeys = parseList(readHeader(headers, INERTIA_HEADERS.reset));
|
|
118
|
-
return {
|
|
119
|
-
isInertia: inertia === "true" || inertia === "1",
|
|
120
|
-
version,
|
|
121
|
-
partialComponent,
|
|
122
|
-
partialOnly,
|
|
123
|
-
partialExcept,
|
|
124
|
-
errorBag,
|
|
125
|
-
resetKeys,
|
|
126
|
-
method: input.method.toUpperCase(),
|
|
127
|
-
url: input.url
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
//#endregion
|
|
131
|
-
//#region src/core/html.ts
|
|
132
|
-
const HTML_ESCAPES = {
|
|
133
|
-
"&": "&",
|
|
134
|
-
"<": "<",
|
|
135
|
-
">": ">",
|
|
136
|
-
"\"": """,
|
|
137
|
-
"'": "'"
|
|
138
|
-
};
|
|
139
|
-
/**
|
|
140
|
-
* Escape characters that would break out of a double-quoted HTML attribute.
|
|
141
|
-
*
|
|
142
|
-
* @param value Raw attribute value.
|
|
143
|
-
*/
|
|
144
|
-
function escapeHtmlAttribute(value) {
|
|
145
|
-
return value.replace(/[&<>"']/g, (ch) => HTML_ESCAPES[ch] ?? ch);
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Encode an Inertia page object for embedding inside a
|
|
149
|
-
* `<script type="application/json" data-page="...">` tag — the Inertia v3
|
|
150
|
-
* initial-page transport that the client reads via `getInitialPageFromDOM`.
|
|
151
|
-
*
|
|
152
|
-
* Every `<` is replaced with its JSON unicode escape so the payload cannot
|
|
153
|
-
* terminate the surrounding `<\/script>` element or open an HTML comment. The
|
|
154
|
-
* escape is transparent — JSON.parse decodes it back to `<`.
|
|
155
|
-
*
|
|
156
|
-
* @param page Resolved Inertia page object.
|
|
157
|
-
*/
|
|
158
|
-
function encodePageScript(page) {
|
|
159
|
-
return JSON.stringify(page).replace(/</g, "\\u003C");
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Minimal default HTML shell for use when no custom root view is provided.
|
|
163
|
-
* Real apps should plug in a template engine (pug, ejs, react-dom/server,
|
|
164
|
-
* etc.) and embed `encodePageScript(page)` themselves.
|
|
165
|
-
*
|
|
166
|
-
* The initial page object is emitted as a
|
|
167
|
-
* `<script type="application/json" data-page="...">` tag, the Inertia v3
|
|
168
|
-
* transport that the client reads on boot.
|
|
169
|
-
*
|
|
170
|
-
* @param input Page object plus optional title/head/rootId/scriptSrc/SSR markup
|
|
171
|
-
* — see {@link DefaultRootViewInput}.
|
|
172
|
-
*/
|
|
173
|
-
function renderDefaultHtml(input) {
|
|
174
|
-
const rootId = input.rootId ?? "app";
|
|
175
|
-
const title = input.title ?? "Inertia App";
|
|
176
|
-
const head = input.head ?? "";
|
|
177
|
-
const script = input.scriptSrc ? `<script type="module" src="${escapeHtmlAttribute(input.scriptSrc)}"><\/script>` : "";
|
|
178
|
-
let rootMarkup;
|
|
179
|
-
if (input.ssrFull) rootMarkup = input.ssrFull;
|
|
180
|
-
else {
|
|
181
|
-
const idAttr = escapeHtmlAttribute(rootId);
|
|
182
|
-
rootMarkup = `<div id="${idAttr}">${input.ssrBody ?? ""}</div>\n ${`<script data-page="${idAttr}" type="application/json">${encodePageScript(input.page)}<\/script>`}`;
|
|
183
|
-
}
|
|
184
|
-
return `<!DOCTYPE html>
|
|
185
|
-
<html>
|
|
186
|
-
<head>
|
|
187
|
-
<meta charset="utf-8" />
|
|
188
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
189
|
-
<title>${escapeHtmlAttribute(title)}</title>
|
|
190
|
-
${head}
|
|
191
|
-
</head>
|
|
192
|
-
<body>
|
|
193
|
-
${rootMarkup}
|
|
194
|
-
${script}
|
|
195
|
-
</body>
|
|
196
|
-
</html>`;
|
|
197
|
-
}
|
|
198
|
-
/** Default user-facing messages keyed by HTTP status, used when none is given. */
|
|
199
|
-
const ERROR_STATUS_MESSAGES = {
|
|
200
|
-
400: "Bad Request",
|
|
201
|
-
401: "Unauthorized",
|
|
202
|
-
403: "Forbidden",
|
|
203
|
-
404: "Page Not Found",
|
|
204
|
-
409: "Conflict",
|
|
205
|
-
419: "Page Expired",
|
|
206
|
-
422: "Unprocessable Entity",
|
|
207
|
-
429: "Too Many Requests",
|
|
208
|
-
500: "Server Error",
|
|
209
|
-
502: "Bad Gateway",
|
|
210
|
-
503: "Service Unavailable",
|
|
211
|
-
504: "Gateway Timeout"
|
|
212
|
-
};
|
|
213
|
-
/**
|
|
214
|
-
* Render a minimal, SPA-less standalone HTML error page. Used as the hard
|
|
215
|
-
* fallback when no client `Error` component can be resolved (e.g. a plain
|
|
216
|
-
* browser load, or an SSR/render failure on the Inertia path), so a user
|
|
217
|
-
* never receives a raw JSON error or a blank page.
|
|
218
|
-
*
|
|
219
|
-
* @param input HTTP status plus an optional override message — see {@link ErrorPageInput}.
|
|
220
|
-
*/
|
|
221
|
-
function renderErrorPage(input) {
|
|
222
|
-
const { status } = input;
|
|
223
|
-
const message = input.message ?? ERROR_STATUS_MESSAGES[status] ?? "Something went wrong";
|
|
224
|
-
const safeStatus = escapeHtmlAttribute(String(status));
|
|
225
|
-
const safeMessage = escapeHtmlAttribute(message);
|
|
226
|
-
return `<!DOCTYPE html>
|
|
227
|
-
<html>
|
|
228
|
-
<head>
|
|
229
|
-
<meta charset="utf-8" />
|
|
230
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
231
|
-
<title>${safeStatus} ${safeMessage}</title>
|
|
232
|
-
<style>
|
|
233
|
-
body { font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f8f9fa; color: #212529; }
|
|
234
|
-
main { text-align: center; padding: 2rem; }
|
|
235
|
-
h1 { font-size: 4rem; margin: 0; font-weight: 600; }
|
|
236
|
-
p { font-size: 1.25rem; margin: 0.5rem 0 0; color: #6c757d; }
|
|
237
|
-
</style>
|
|
238
|
-
</head>
|
|
239
|
-
<body>
|
|
240
|
-
<main>
|
|
241
|
-
<h1>${safeStatus}</h1>
|
|
242
|
-
<p>${safeMessage}</p>
|
|
243
|
-
</main>
|
|
244
|
-
</body>
|
|
245
|
-
</html>`;
|
|
246
|
-
}
|
|
247
|
-
//#endregion
|
|
248
|
-
//#region src/core/page.ts
|
|
249
|
-
/**
|
|
250
|
-
* Whether flash data is worth emitting as a top-level page key. Nullish and
|
|
251
|
-
* empty-object flash are dropped so the wire payload stays compact and
|
|
252
|
-
* absent flash never surfaces a stray `flash: {}` to the client.
|
|
253
|
-
*
|
|
254
|
-
* @param flash Candidate flash value pulled from the session.
|
|
255
|
-
*/
|
|
256
|
-
function hasFlashData(flash) {
|
|
257
|
-
if (flash === null || flash === void 0) return false;
|
|
258
|
-
if (typeof flash === "object" && !Array.isArray(flash)) return Object.keys(flash).length > 0;
|
|
259
|
-
return true;
|
|
260
|
-
}
|
|
261
|
-
/**
|
|
262
|
-
* Build a normalized Inertia v3 page object. `clearHistory` and `encryptHistory`
|
|
263
|
-
* default to `false`. Empty/absent `deferredProps` / `mergeProps` /
|
|
264
|
-
* `deepMergeProps` / `matchPropsOn` are dropped so the JSON over the wire stays
|
|
265
|
-
* compact. `flash` is emitted as a top-level key only when it carries data
|
|
266
|
-
* (non-nullish, and a non-empty object when object-valued) — mirroring
|
|
267
|
-
* inertia-laravel's `$flash ? ['flash' => $flash] : []`.
|
|
268
|
-
*
|
|
269
|
-
* @param input Required page fields plus the optional v3 extras.
|
|
270
|
-
*/
|
|
271
|
-
function createPage(input) {
|
|
272
|
-
const page = {
|
|
273
|
-
component: input.component,
|
|
274
|
-
props: input.props,
|
|
275
|
-
url: input.url,
|
|
276
|
-
version: input.version,
|
|
277
|
-
clearHistory: input.clearHistory ?? false,
|
|
278
|
-
encryptHistory: input.encryptHistory ?? false
|
|
279
|
-
};
|
|
280
|
-
if (input.deferredProps && Object.keys(input.deferredProps).length > 0) page.deferredProps = input.deferredProps;
|
|
281
|
-
if (input.mergeProps && input.mergeProps.length > 0) page.mergeProps = input.mergeProps;
|
|
282
|
-
if (input.deepMergeProps && input.deepMergeProps.length > 0) page.deepMergeProps = input.deepMergeProps;
|
|
283
|
-
if (input.matchPropsOn && input.matchPropsOn.length > 0) page.matchPropsOn = input.matchPropsOn;
|
|
284
|
-
if (hasFlashData(input.flash)) page.flash = input.flash;
|
|
285
|
-
return page;
|
|
286
|
-
}
|
|
287
|
-
//#endregion
|
|
288
|
-
//#region src/core/props.ts
|
|
289
|
-
const LAZY = "__inertia_lazy";
|
|
290
|
-
const OPTIONAL = "__inertia_optional";
|
|
291
|
-
const DEFERRED = "__inertia_deferred";
|
|
292
|
-
const ALWAYS = "__inertia_always";
|
|
293
|
-
const MERGE = "__inertia_merge";
|
|
294
|
-
const DEEP_MERGE = "__inertia_deep_merge";
|
|
295
|
-
const MATCH_ON = "__inertia_match_on";
|
|
296
|
-
/**
|
|
297
|
-
* Attach the (non-enumerable) `MATCH_ON` symbol-key to a merge/deep-merge
|
|
298
|
-
* prop value so `resolveProps` can emit the dotted `prop.field` paths.
|
|
299
|
-
*
|
|
300
|
-
* @param value Object being tagged (mutated in place).
|
|
301
|
-
* @param matchOn Single field name or list; no-op when undefined.
|
|
302
|
-
*/
|
|
303
|
-
function tagMatchOn(value, matchOn) {
|
|
304
|
-
if (matchOn === void 0) return;
|
|
305
|
-
Object.defineProperty(value, MATCH_ON, {
|
|
306
|
-
value: Array.isArray(matchOn) ? matchOn : [matchOn],
|
|
307
|
-
enumerable: false
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
/**
|
|
311
|
-
* Wrap a value-producing function as a "lazy" prop.
|
|
312
|
-
*
|
|
313
|
-
* Lazy props are evaluated on every visit (full or partial) when included.
|
|
314
|
-
* They are NOT skipped on the initial visit — use {@link optional} for that.
|
|
315
|
-
*
|
|
316
|
-
* @param fn Producer called (and awaited) when the prop is included.
|
|
317
|
-
*/
|
|
318
|
-
function lazy(fn) {
|
|
319
|
-
const wrapped = (async () => fn());
|
|
320
|
-
Object.defineProperty(wrapped, LAZY, {
|
|
321
|
-
value: true,
|
|
322
|
-
enumerable: false
|
|
323
|
-
});
|
|
324
|
-
return wrapped;
|
|
325
|
-
}
|
|
326
|
-
/**
|
|
327
|
-
* Wrap a value-producing function as an "optional" prop.
|
|
328
|
-
*
|
|
329
|
-
* Optional props are omitted from the initial response and only included
|
|
330
|
-
* when explicitly requested via `X-Inertia-Partial-Data`.
|
|
331
|
-
*
|
|
332
|
-
* @param fn Producer called (and awaited) on partial reloads that request the key.
|
|
333
|
-
*/
|
|
334
|
-
function optional(fn) {
|
|
335
|
-
const wrapped = (async () => fn());
|
|
336
|
-
Object.defineProperty(wrapped, OPTIONAL, {
|
|
337
|
-
value: true,
|
|
338
|
-
enumerable: false
|
|
339
|
-
});
|
|
340
|
-
return wrapped;
|
|
341
|
-
}
|
|
342
|
-
/**
|
|
343
|
-
* Wrap a value-producing function as a "deferred" prop.
|
|
344
|
-
*
|
|
345
|
-
* Deferred props are omitted from the initial visit. The client receives
|
|
346
|
-
* the list of deferred prop keys (grouped by `group`) in the page object
|
|
347
|
-
* and triggers an automatic partial reload to fetch them.
|
|
348
|
-
*
|
|
349
|
-
* @param fn Producer called (and awaited) on the deferred follow-up reload.
|
|
350
|
-
* @param group Load-group name; keys in the same group are fetched in one round-trip.
|
|
351
|
-
*/
|
|
352
|
-
function defer(fn, group = "default") {
|
|
353
|
-
const wrapped = (async () => fn());
|
|
354
|
-
Object.defineProperty(wrapped, DEFERRED, {
|
|
355
|
-
value: true,
|
|
356
|
-
enumerable: false
|
|
357
|
-
});
|
|
358
|
-
Object.defineProperty(wrapped, "group", {
|
|
359
|
-
value: group,
|
|
360
|
-
enumerable: false
|
|
361
|
-
});
|
|
362
|
-
return wrapped;
|
|
363
|
-
}
|
|
364
|
-
/**
|
|
365
|
-
* Wrap a value-producing function as an "always" prop.
|
|
366
|
-
*
|
|
367
|
-
* Always props are included on every response — full visits and partial
|
|
368
|
-
* reloads alike — regardless of `X-Inertia-Partial-Data` /
|
|
369
|
-
* `X-Inertia-Partial-Except` filtering.
|
|
370
|
-
*
|
|
371
|
-
* @param fn Producer called (and awaited) on every visit.
|
|
372
|
-
*/
|
|
373
|
-
function always(fn) {
|
|
374
|
-
const wrapped = (async () => fn());
|
|
375
|
-
Object.defineProperty(wrapped, ALWAYS, {
|
|
376
|
-
value: true,
|
|
377
|
-
enumerable: false
|
|
378
|
-
});
|
|
379
|
-
return wrapped;
|
|
380
|
-
}
|
|
381
|
-
/**
|
|
382
|
-
* Mark a value as a "merge" prop. The Inertia client will append the new
|
|
383
|
-
* value to the existing prop (typically used for paginated arrays).
|
|
384
|
-
*
|
|
385
|
-
* Pass `matchOn` (a field name or list of field names) to have the client
|
|
386
|
-
* dedupe array items on that field instead of blindly appending.
|
|
387
|
-
*
|
|
388
|
-
* @param value Object or array to tag (mutated in place via non-enumerable properties).
|
|
389
|
-
* @param matchOn Optional field name(s) to dedupe on.
|
|
390
|
-
*/
|
|
391
|
-
function merge(value, matchOn) {
|
|
392
|
-
const tagged = value;
|
|
393
|
-
Object.defineProperty(tagged, MERGE, {
|
|
394
|
-
value: true,
|
|
395
|
-
enumerable: false
|
|
396
|
-
});
|
|
397
|
-
tagMatchOn(tagged, matchOn);
|
|
398
|
-
return tagged;
|
|
399
|
-
}
|
|
400
|
-
/**
|
|
401
|
-
* Mark a value as a "deep merge" prop. The Inertia client recursively merges
|
|
402
|
-
* the new value into the existing prop rather than shallow-appending.
|
|
403
|
-
*
|
|
404
|
-
* Pass `matchOn` (a field name or list of field names) to have the client
|
|
405
|
-
* dedupe array items on that field.
|
|
406
|
-
*
|
|
407
|
-
* @param value Object or array to tag (mutated in place via non-enumerable properties).
|
|
408
|
-
* @param matchOn Optional field name(s) to dedupe on.
|
|
409
|
-
*/
|
|
410
|
-
function deepMerge(value, matchOn) {
|
|
411
|
-
const tagged = value;
|
|
412
|
-
Object.defineProperty(tagged, DEEP_MERGE, {
|
|
413
|
-
value: true,
|
|
414
|
-
enumerable: false
|
|
415
|
-
});
|
|
416
|
-
tagMatchOn(tagged, matchOn);
|
|
417
|
-
return tagged;
|
|
418
|
-
}
|
|
419
|
-
/**
|
|
420
|
-
* Type guard: `true` when `v` is a {@link lazy} prop wrapper.
|
|
421
|
-
*
|
|
422
|
-
* @param v Value to test.
|
|
423
|
-
*/
|
|
424
|
-
function isLazy(v) {
|
|
425
|
-
return typeof v === "function" && v[LAZY] === true;
|
|
426
|
-
}
|
|
427
|
-
/**
|
|
428
|
-
* Type guard: `true` when `v` is an {@link optional} prop wrapper.
|
|
429
|
-
*
|
|
430
|
-
* @param v Value to test.
|
|
431
|
-
*/
|
|
432
|
-
function isOptional(v) {
|
|
433
|
-
return typeof v === "function" && v[OPTIONAL] === true;
|
|
434
|
-
}
|
|
435
|
-
/**
|
|
436
|
-
* Type guard: `true` when `v` is a {@link defer} prop wrapper.
|
|
437
|
-
*
|
|
438
|
-
* @param v Value to test.
|
|
439
|
-
*/
|
|
440
|
-
function isDeferred(v) {
|
|
441
|
-
return typeof v === "function" && v[DEFERRED] === true;
|
|
442
|
-
}
|
|
443
|
-
/**
|
|
444
|
-
* Type guard: `true` when `v` is an {@link always} prop wrapper.
|
|
445
|
-
*
|
|
446
|
-
* @param v Value to test.
|
|
447
|
-
*/
|
|
448
|
-
function isAlways(v) {
|
|
449
|
-
return typeof v === "function" && v[ALWAYS] === true;
|
|
450
|
-
}
|
|
451
|
-
/**
|
|
452
|
-
* Type guard: `true` when `v` is a {@link merge} prop wrapper.
|
|
453
|
-
*
|
|
454
|
-
* @param v Value to test.
|
|
455
|
-
*/
|
|
456
|
-
function isMerge(v) {
|
|
457
|
-
return v !== null && typeof v === "object" && v[MERGE] === true;
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* Type guard: `true` when `v` is a {@link deepMerge} prop wrapper.
|
|
461
|
-
*
|
|
462
|
-
* @param v Value to test.
|
|
463
|
-
*/
|
|
464
|
-
function isDeepMerge(v) {
|
|
465
|
-
return v !== null && typeof v === "object" && v[DEEP_MERGE] === true;
|
|
466
|
-
}
|
|
467
|
-
/**
|
|
468
|
-
* Match a prop key against a partial-reload key list. A pattern matches when
|
|
469
|
-
* it equals the key, or when it is a dotted path into the key (`users.data`
|
|
470
|
-
* matches the `users` prop). Mirrors the official adapter's partial filtering.
|
|
471
|
-
*
|
|
472
|
-
* @param patterns Key patterns from `X-Inertia-Partial-Data` / `-Partial-Except`.
|
|
473
|
-
* @param key Prop key being tested.
|
|
474
|
-
*/
|
|
475
|
-
function partialKeyMatches(patterns, key) {
|
|
476
|
-
return patterns.some((pattern) => pattern === key || pattern.startsWith(`${key}.`));
|
|
477
|
-
}
|
|
478
|
-
/**
|
|
479
|
-
* Read the non-enumerable `MATCH_ON` field-list off a tagged merge value,
|
|
480
|
-
* returning an empty array when absent.
|
|
481
|
-
*
|
|
482
|
-
* @param v Possibly-tagged merge / deep-merge prop value.
|
|
483
|
-
*/
|
|
484
|
-
function readMatchOn(v) {
|
|
485
|
-
if (v === null || typeof v !== "object") return [];
|
|
486
|
-
const fields = v[MATCH_ON];
|
|
487
|
-
return Array.isArray(fields) ? fields : [];
|
|
488
|
-
}
|
|
489
|
-
/**
|
|
490
|
-
* Combine shared + page props, then apply partial-reload filtering and
|
|
491
|
-
* lazy/optional/deferred evaluation per the Inertia v3 protocol.
|
|
492
|
-
*
|
|
493
|
-
* Resolution order matches the official adapters:
|
|
494
|
-
* 1. Merge shared and page props (page wins on collision).
|
|
495
|
-
* 2. Determine if this is a partial reload for the same component.
|
|
496
|
-
* 3. For non-partial visits: drop optional and deferred props; keep everything
|
|
497
|
-
* else (lazy props are evaluated).
|
|
498
|
-
* 4. For partial visits: keep only keys in `partialOnly` (if set), then drop
|
|
499
|
-
* keys in `partialExcept`. Optional props are now eligible.
|
|
500
|
-
* 5. Evaluate every remaining function-shaped prop
|
|
501
|
-
* (lazy/optional/deferred/always) by calling it and awaiting the result.
|
|
502
|
-
* Independent props are evaluated in parallel via `Promise.all`.
|
|
503
|
-
* 6. Collect merge / deep-merge / match-on keys, excluding any key the client
|
|
504
|
-
* listed in `X-Inertia-Reset` — those props are reset (replaced) rather
|
|
505
|
-
* than appended.
|
|
506
|
-
*
|
|
507
|
-
* @param input Page + shared props, the component name, and parsed request info.
|
|
508
|
-
*/
|
|
509
|
-
async function resolveProps(input) {
|
|
510
|
-
const { props, shared, component, request } = input;
|
|
511
|
-
const combined = {
|
|
512
|
-
...shared,
|
|
513
|
-
...props
|
|
514
|
-
};
|
|
515
|
-
const isPartial = request.isInertia && request.partialComponent !== null && request.partialComponent === component;
|
|
516
|
-
const deferred = {};
|
|
517
|
-
const mergeKeys = [];
|
|
518
|
-
const deepMergeKeys = [];
|
|
519
|
-
const matchOnKeys = [];
|
|
520
|
-
const filtered = {};
|
|
521
|
-
const isExcludedFromPartial = (key, value) => {
|
|
522
|
-
if (!isPartial || isAlways(value)) return false;
|
|
523
|
-
if (request.partialOnly !== null && !partialKeyMatches(request.partialOnly, key)) return true;
|
|
524
|
-
return request.partialExcept !== null && partialKeyMatches(request.partialExcept, key);
|
|
525
|
-
};
|
|
526
|
-
Object.entries(combined).forEach(([key, value]) => {
|
|
527
|
-
if (isDeferred(value) && !isPartial) {
|
|
528
|
-
(deferred[value.group] ??= []).push(key);
|
|
529
|
-
return;
|
|
530
|
-
}
|
|
531
|
-
if (isOptional(value) && !isPartial) return;
|
|
532
|
-
if (isExcludedFromPartial(key, value)) return;
|
|
533
|
-
filtered[key] = value;
|
|
534
|
-
});
|
|
535
|
-
const { resetKeys } = request;
|
|
536
|
-
const evaluated = {};
|
|
537
|
-
(await Promise.all(Object.entries(filtered).map(async ([key, value]) => {
|
|
538
|
-
return [key, isLazy(value) || isOptional(value) || isDeferred(value) || isAlways(value) ? await value() : value];
|
|
539
|
-
}))).forEach(([key, resolved]) => {
|
|
540
|
-
evaluated[key] = resolved;
|
|
541
|
-
if (resetKeys?.includes(key) ?? false) return;
|
|
542
|
-
const merging = isMerge(resolved);
|
|
543
|
-
const deepMerging = isDeepMerge(resolved);
|
|
544
|
-
if (merging) mergeKeys.push(key);
|
|
545
|
-
else if (deepMerging) deepMergeKeys.push(key);
|
|
546
|
-
if (merging || deepMerging) readMatchOn(resolved).forEach((field) => {
|
|
547
|
-
matchOnKeys.push(`${key}.${field}`);
|
|
548
|
-
});
|
|
549
|
-
});
|
|
550
|
-
return {
|
|
551
|
-
props: evaluated,
|
|
552
|
-
deferred,
|
|
553
|
-
merge: mergeKeys,
|
|
554
|
-
deepMerge: deepMergeKeys,
|
|
555
|
-
matchOn: matchOnKeys
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
//#endregion
|
|
559
|
-
//#region src/core/redirects.ts
|
|
560
|
-
/**
|
|
561
|
-
* Status code Inertia uses for "soft" redirects after PUT/PATCH/DELETE so the
|
|
562
|
-
* follow-up GET is forced to also be a GET on every browser.
|
|
563
|
-
*/
|
|
564
|
-
const INERTIA_REDIRECT_STATUS = 303;
|
|
565
|
-
/**
|
|
566
|
-
* Status code used for the `X-Inertia-Location` external redirect
|
|
567
|
-
* mechanism. The client intercepts the 409 and performs a window.location
|
|
568
|
-
* navigation rather than a fetch.
|
|
569
|
-
*/
|
|
570
|
-
const INERTIA_LOCATION_STATUS = 409;
|
|
571
|
-
/**
|
|
572
|
-
* Decide which status code to use for a redirect produced by an Inertia
|
|
573
|
-
* request. After a non-GET/HEAD form submission the response must be 303
|
|
574
|
-
* so the client's follow-up request becomes a GET.
|
|
575
|
-
*
|
|
576
|
-
* @param method HTTP method of the inbound request (case-insensitive).
|
|
577
|
-
* @param fallback Status code to use for GET/HEAD/POST. Defaults to 302.
|
|
578
|
-
*/
|
|
579
|
-
function inertiaRedirectStatus(method, fallback = 302) {
|
|
580
|
-
const m = method.toUpperCase();
|
|
581
|
-
if (m === "PUT" || m === "PATCH" || m === "DELETE") return 303;
|
|
582
|
-
return fallback;
|
|
583
|
-
}
|
|
584
|
-
//#endregion
|
|
585
|
-
//#region src/core/render.ts
|
|
586
|
-
/**
|
|
587
|
-
* Resolve a `SharedPropsInput` to a plain props object. Returns `{}` for
|
|
588
|
-
* undefined/null inputs or when the resolver returns nullish.
|
|
589
|
-
*
|
|
590
|
-
* @param shared Static props object, a (possibly async) function, or undefined.
|
|
591
|
-
* @param req Request passed to the resolver when `shared` is a function.
|
|
592
|
-
*/
|
|
593
|
-
async function evaluateShared(shared, req) {
|
|
594
|
-
if (shared === void 0 || shared === null) return {};
|
|
595
|
-
if (typeof shared === "function") return await shared(req) ?? {};
|
|
596
|
-
return shared;
|
|
597
|
-
}
|
|
598
|
-
/**
|
|
599
|
-
* Build the final {@link InertiaPage} object given a controller's component
|
|
600
|
-
* + props, the request info, and configured shared props / version.
|
|
601
|
-
*
|
|
602
|
-
* Returned page is suitable for either:
|
|
603
|
-
* - JSON serialization on `X-Inertia` requests, or
|
|
604
|
-
* - embedding into the root HTML view via a `<script>` page tag.
|
|
605
|
-
*
|
|
606
|
-
* @param input Component, props, parsed request, shared resolver, version, and
|
|
607
|
-
* optional `clearHistory` / `encryptHistory` flags.
|
|
608
|
-
*/
|
|
609
|
-
async function buildPage(input) {
|
|
610
|
-
const sharedProps = await evaluateShared(input.shared, input.req);
|
|
611
|
-
const resolved = await resolveProps({
|
|
612
|
-
props: input.props,
|
|
613
|
-
shared: sharedProps,
|
|
614
|
-
component: input.component,
|
|
615
|
-
request: input.request
|
|
616
|
-
});
|
|
617
|
-
return createPage({
|
|
618
|
-
component: input.component,
|
|
619
|
-
props: resolved.props,
|
|
620
|
-
url: input.request.url,
|
|
621
|
-
version: input.version,
|
|
622
|
-
clearHistory: input.options?.clearHistory,
|
|
623
|
-
encryptHistory: input.options?.encryptHistory,
|
|
624
|
-
deferredProps: resolved.deferred,
|
|
625
|
-
mergeProps: resolved.merge,
|
|
626
|
-
deepMergeProps: resolved.deepMerge,
|
|
627
|
-
matchPropsOn: resolved.matchOn,
|
|
628
|
-
flash: input.flash
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
//#endregion
|
|
632
|
-
//#region src/core/versioning.ts
|
|
633
|
-
/**
|
|
634
|
-
* Resolve a version value (string, async function, or null) against a request.
|
|
635
|
-
*
|
|
636
|
-
* Returns `null` when no version is configured — in which case asset version
|
|
637
|
-
* mismatch detection is disabled.
|
|
638
|
-
*
|
|
639
|
-
* @param resolver Static version string, a (possibly async) function, or undefined.
|
|
640
|
-
* @param req Request passed to the resolver when it is a function.
|
|
641
|
-
*/
|
|
642
|
-
async function resolveVersion(resolver, req) {
|
|
643
|
-
if (resolver === void 0 || resolver === null) return null;
|
|
644
|
-
if (typeof resolver === "string") return resolver;
|
|
645
|
-
return await resolver(req) ?? null;
|
|
646
|
-
}
|
|
647
|
-
/**
|
|
648
|
-
* Returns true if the client's version (`X-Inertia-Version`) does not match
|
|
649
|
-
* the server's current version. Mismatches on GET requests should trigger a
|
|
650
|
-
* full reload via the `X-Inertia-Location` mechanism.
|
|
651
|
-
*
|
|
652
|
-
* @param serverVersion Currently configured server version; `null` disables checks.
|
|
653
|
-
* @param clientVersion Value of the `X-Inertia-Version` header; `null` is treated as empty.
|
|
654
|
-
*/
|
|
655
|
-
function isVersionMismatch(serverVersion, clientVersion) {
|
|
656
|
-
if (serverVersion === null) return false;
|
|
657
|
-
return serverVersion !== (clientVersion ?? "");
|
|
658
|
-
}
|
|
659
|
-
//#endregion
|
|
660
|
-
export { INERTIA_HEADERS, INERTIA_LOCATION_STATUS, INERTIA_REDIRECT_STATUS, always, buildPage, canonicalStringify, computeSsrCacheKey, createPage, deepMerge, defer, encodePageScript, inertiaRedirectStatus, isVersionMismatch, lazy, merge, optional, parseInertiaRequest, renderDefaultHtml, renderErrorPage, resolveProps, resolveVersion, scopeErrors };
|
|
661
|
-
|
|
662
|
-
//# sourceMappingURL=index.js.map
|
|
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 };
|