@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
|
@@ -0,0 +1,1499 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
//#region src/core/cache.ts
|
|
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/prop-descriptor.ts
|
|
47
|
+
/**
|
|
48
|
+
* A prop wrapped with resolution and merge behaviour, ported from
|
|
49
|
+
* inertia-laravel's prop classes and their `MergesProps`, `DefersProps`
|
|
50
|
+
* and `ResolvesOnce` traits. The descriptor holds the wrapped value by
|
|
51
|
+
* reference and never mutates it.
|
|
52
|
+
*/
|
|
53
|
+
var PropDescriptor = class {
|
|
54
|
+
kind;
|
|
55
|
+
source;
|
|
56
|
+
ignoreFirstLoad = false;
|
|
57
|
+
defers = false;
|
|
58
|
+
group = "default";
|
|
59
|
+
merges = false;
|
|
60
|
+
deep = false;
|
|
61
|
+
appends = true;
|
|
62
|
+
appendPaths = [];
|
|
63
|
+
prependPaths = [];
|
|
64
|
+
matchOnPaths = [];
|
|
65
|
+
resolvesOnce = false;
|
|
66
|
+
onceKey = null;
|
|
67
|
+
ttlSeconds = null;
|
|
68
|
+
expiresAtDate = null;
|
|
69
|
+
isFresh = false;
|
|
70
|
+
rescues = false;
|
|
71
|
+
isAlways = false;
|
|
72
|
+
isScroll = false;
|
|
73
|
+
wrapper = "data";
|
|
74
|
+
scrollMetadata = null;
|
|
75
|
+
/**
|
|
76
|
+
* Create a descriptor around a prop source.
|
|
77
|
+
*
|
|
78
|
+
* @param kind The constructor that produced this descriptor.
|
|
79
|
+
* @param source The plain value or producer function being wrapped.
|
|
80
|
+
*/
|
|
81
|
+
constructor(kind, source) {
|
|
82
|
+
this.kind = kind;
|
|
83
|
+
this.source = source;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* `true` when the prop appends at its root: the append flag is set and no
|
|
87
|
+
* append/prepend paths were recorded.
|
|
88
|
+
*/
|
|
89
|
+
get appendsAtRoot() {
|
|
90
|
+
return this.appends && this.mergesAtRoot();
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* `true` when the prop prepends at its root: the append flag is cleared and
|
|
94
|
+
* no append/prepend paths were recorded.
|
|
95
|
+
*/
|
|
96
|
+
get prependsAtRoot() {
|
|
97
|
+
return !this.appends && this.mergesAtRoot();
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Mark the prop for a shallow client-side merge.
|
|
101
|
+
*/
|
|
102
|
+
merge() {
|
|
103
|
+
this.merges = true;
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Mark the prop for a deep client-side merge.
|
|
108
|
+
*/
|
|
109
|
+
deepMerge() {
|
|
110
|
+
this.deep = true;
|
|
111
|
+
return this.merge();
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Configure appending. A boolean sets the root append flag; a string
|
|
115
|
+
* records an append path; a list records each path; a record maps each
|
|
116
|
+
* path to its match-on field.
|
|
117
|
+
*
|
|
118
|
+
* @param path Root flag, path, list of paths, or path-to-matchOn record.
|
|
119
|
+
* @param matchOn Field to dedupe on, recorded as `path.matchOn` when `path` is a string.
|
|
120
|
+
*/
|
|
121
|
+
append(path = true, matchOn) {
|
|
122
|
+
if (typeof path === "boolean") this.appends = path;
|
|
123
|
+
else if (typeof path === "string") {
|
|
124
|
+
this.appendPaths.push(path);
|
|
125
|
+
this.recordMatchOn(path, matchOn);
|
|
126
|
+
} else if (Array.isArray(path)) path.forEach((entry) => this.append(entry));
|
|
127
|
+
else Object.entries(path).forEach(([key, field]) => this.append(key, field));
|
|
128
|
+
return this;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Configure prepending. A boolean clears (or sets) the root append flag; a
|
|
132
|
+
* string records a prepend path; a list records each path; a record maps
|
|
133
|
+
* each path to its match-on field.
|
|
134
|
+
*
|
|
135
|
+
* @param path Root flag, path, list of paths, or path-to-matchOn record.
|
|
136
|
+
* @param matchOn Field to dedupe on, recorded as `path.matchOn` when `path` is a string.
|
|
137
|
+
*/
|
|
138
|
+
prepend(path = true, matchOn) {
|
|
139
|
+
if (typeof path === "boolean") this.appends = !path;
|
|
140
|
+
else if (typeof path === "string") {
|
|
141
|
+
this.prependPaths.push(path);
|
|
142
|
+
this.recordMatchOn(path, matchOn);
|
|
143
|
+
} else if (Array.isArray(path)) path.forEach((entry) => this.prepend(entry));
|
|
144
|
+
else Object.entries(path).forEach(([key, field]) => this.prepend(key, field));
|
|
145
|
+
return this;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Replace the match-on paths the client dedupes merged items by.
|
|
149
|
+
*
|
|
150
|
+
* @param path A single path or a list of paths.
|
|
151
|
+
*/
|
|
152
|
+
matchOn(path) {
|
|
153
|
+
this.matchOnPaths = Array.isArray(path) ? [...path] : [path];
|
|
154
|
+
return this;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Defer the prop to a follow-up request.
|
|
158
|
+
*
|
|
159
|
+
* @param group Load group; props sharing a group are fetched together. Defaults to `'default'`.
|
|
160
|
+
*/
|
|
161
|
+
defer(group) {
|
|
162
|
+
this.defers = true;
|
|
163
|
+
this.group = group ?? "default";
|
|
164
|
+
return this;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Toggle once-resolution, optionally setting its key and expiry.
|
|
168
|
+
*
|
|
169
|
+
* @param value Whether the prop resolves once. Defaults to `true`.
|
|
170
|
+
* @param as Custom once key.
|
|
171
|
+
* @param until TTL in seconds, or an absolute expiry date.
|
|
172
|
+
*/
|
|
173
|
+
once(value = true, as, until) {
|
|
174
|
+
this.resolvesOnce = value;
|
|
175
|
+
if (as !== void 0) this.as(as);
|
|
176
|
+
if (until !== void 0) this.until(until);
|
|
177
|
+
return this;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Set the key the client caches a once-prop under.
|
|
181
|
+
*
|
|
182
|
+
* @param key The once key.
|
|
183
|
+
*/
|
|
184
|
+
as(key) {
|
|
185
|
+
this.onceKey = key;
|
|
186
|
+
return this;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Set the once-prop's expiry, replacing any expiry set by an earlier call.
|
|
190
|
+
*
|
|
191
|
+
* @param delay TTL in seconds, or an absolute expiry date.
|
|
192
|
+
*/
|
|
193
|
+
until(delay) {
|
|
194
|
+
if (delay instanceof Date) {
|
|
195
|
+
this.expiresAtDate = delay;
|
|
196
|
+
this.ttlSeconds = null;
|
|
197
|
+
} else {
|
|
198
|
+
this.ttlSeconds = delay;
|
|
199
|
+
this.expiresAtDate = null;
|
|
200
|
+
}
|
|
201
|
+
return this;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Force a once-prop to be re-resolved.
|
|
205
|
+
*
|
|
206
|
+
* @param value Whether to refresh. Defaults to `true`.
|
|
207
|
+
*/
|
|
208
|
+
fresh(value = true) {
|
|
209
|
+
this.isFresh = value;
|
|
210
|
+
return this;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Opt into rescuing errors thrown while resolving the prop.
|
|
214
|
+
*
|
|
215
|
+
* @param value Whether to rescue. Defaults to `true`.
|
|
216
|
+
*/
|
|
217
|
+
rescue(value = true) {
|
|
218
|
+
this.rescues = value;
|
|
219
|
+
return this;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The once-prop's expiry as a millisecond epoch, or `null` without a TTL or date.
|
|
223
|
+
*
|
|
224
|
+
* @param now Current time in milliseconds. Defaults to `Date.now()`.
|
|
225
|
+
*/
|
|
226
|
+
expiresAt(now = Date.now()) {
|
|
227
|
+
if (this.expiresAtDate !== null) return this.expiresAtDate.getTime();
|
|
228
|
+
if (this.ttlSeconds !== null) return now + this.ttlSeconds * 1e3;
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Resolve the source: call it when it is a function, then await the result.
|
|
233
|
+
*/
|
|
234
|
+
async resolveSource() {
|
|
235
|
+
if (typeof this.source === "function") return this.source();
|
|
236
|
+
return this.source;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Scroll metadata for a resolved value: the configured object, or the
|
|
240
|
+
* configured function applied to `resolved`.
|
|
241
|
+
*
|
|
242
|
+
* @param resolved The resolved prop value.
|
|
243
|
+
*/
|
|
244
|
+
metadataFor(resolved) {
|
|
245
|
+
if (this.scrollMetadata === null) throw new TypeError(`Prop of kind "${this.kind}" has no scroll metadata.`);
|
|
246
|
+
return typeof this.scrollMetadata === "function" ? this.scrollMetadata(resolved) : this.scrollMetadata;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* `true` when no append or prepend paths were recorded.
|
|
250
|
+
*/
|
|
251
|
+
mergesAtRoot() {
|
|
252
|
+
return this.appendPaths.length === 0 && this.prependPaths.length === 0;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Record `path.matchOn` as a match-on path when `matchOn` is given.
|
|
256
|
+
*
|
|
257
|
+
* @param path The append/prepend path.
|
|
258
|
+
* @param matchOn Field to dedupe on.
|
|
259
|
+
*/
|
|
260
|
+
recordMatchOn(path, matchOn) {
|
|
261
|
+
if (matchOn) this.matchOnPaths.push(`${path}.${matchOn}`);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
//#endregion
|
|
265
|
+
//#region src/core/props.ts
|
|
266
|
+
/**
|
|
267
|
+
* Throw a `TypeError` unless `fn` is callable.
|
|
268
|
+
*
|
|
269
|
+
* @param name Constructor name, for the error message.
|
|
270
|
+
* @param fn Value that must be a function.
|
|
271
|
+
*/
|
|
272
|
+
function assertCallable(name, fn) {
|
|
273
|
+
if (typeof fn !== "function") throw new TypeError(`${name}() expects a function, received ${typeof fn}.`);
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Throw a `TypeError` when a merge constructor received extra arguments
|
|
277
|
+
* (the 2.x `merge(value, matchOn)` form), so migrating callers fail loudly
|
|
278
|
+
* instead of silently losing their dedupe key.
|
|
279
|
+
*
|
|
280
|
+
* @param name Constructor name, for the error message.
|
|
281
|
+
* @param extra Arguments passed after the value.
|
|
282
|
+
*/
|
|
283
|
+
function assertNoMatchOnArgument(name, extra) {
|
|
284
|
+
if (extra.length > 0) throw new TypeError(`${name}() takes a single argument; use ${name}(value).matchOn(...) to dedupe merged items.`);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* An optional prop: omitted from the first load, resolved when requested.
|
|
288
|
+
*
|
|
289
|
+
* @param fn Producer called (and awaited) when the prop is requested.
|
|
290
|
+
*/
|
|
291
|
+
function optional(fn) {
|
|
292
|
+
assertCallable("optional", fn);
|
|
293
|
+
const prop = new PropDescriptor("optional", fn);
|
|
294
|
+
prop.ignoreFirstLoad = true;
|
|
295
|
+
return prop;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* A deferred prop: omitted from the first load and fetched by the client in
|
|
299
|
+
* a follow-up request with the rest of its group.
|
|
300
|
+
*
|
|
301
|
+
* @param fn Producer called (and awaited) on the follow-up request.
|
|
302
|
+
* @param group Load group; props sharing a group are fetched together.
|
|
303
|
+
*/
|
|
304
|
+
function defer(fn, group = "default") {
|
|
305
|
+
assertCallable("defer", fn);
|
|
306
|
+
const prop = new PropDescriptor("defer", fn);
|
|
307
|
+
prop.ignoreFirstLoad = true;
|
|
308
|
+
return prop.defer(group);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* An always prop: included on every response, even partial reloads that do
|
|
312
|
+
* not request it.
|
|
313
|
+
*
|
|
314
|
+
* @param value Plain value or producer.
|
|
315
|
+
*/
|
|
316
|
+
function always(value) {
|
|
317
|
+
const prop = new PropDescriptor("always", value);
|
|
318
|
+
prop.isAlways = true;
|
|
319
|
+
return prop;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* A merge prop: the client appends it to its existing value at the root.
|
|
323
|
+
* Throws a `TypeError` when given a second argument (the 2.x `matchOn`
|
|
324
|
+
* form); chain `.matchOn()` instead.
|
|
325
|
+
*
|
|
326
|
+
* @param value Plain value or producer.
|
|
327
|
+
* @param extra Rejected; present only to catch the 2.x call form.
|
|
328
|
+
*/
|
|
329
|
+
function merge(value, ...extra) {
|
|
330
|
+
assertNoMatchOnArgument("merge", extra);
|
|
331
|
+
return new PropDescriptor("merge", value).merge();
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* A deep-merge prop: the client recursively merges it into its existing value.
|
|
335
|
+
* Throws a `TypeError` when given a second argument (the 2.x `matchOn`
|
|
336
|
+
* form); chain `.matchOn()` instead.
|
|
337
|
+
*
|
|
338
|
+
* @param value Plain value or producer.
|
|
339
|
+
* @param extra Rejected; present only to catch the 2.x call form.
|
|
340
|
+
*/
|
|
341
|
+
function deepMerge(value, ...extra) {
|
|
342
|
+
assertNoMatchOnArgument("deepMerge", extra);
|
|
343
|
+
return new PropDescriptor("merge", value).deepMerge();
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* A once prop: resolved once and cached by the client across visits.
|
|
347
|
+
*
|
|
348
|
+
* @param fn Producer called (and awaited) when the client lacks the value.
|
|
349
|
+
*/
|
|
350
|
+
function once(fn) {
|
|
351
|
+
assertCallable("once", fn);
|
|
352
|
+
return new PropDescriptor("once", fn).once();
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* An infinite-scroll prop: merged at its wrapper key and carrying
|
|
356
|
+
* pagination metadata.
|
|
357
|
+
*
|
|
358
|
+
* @param fn Producer called (and awaited) for each page.
|
|
359
|
+
* @param options Wrapper key (default `'data'`) and pagination metadata.
|
|
360
|
+
*/
|
|
361
|
+
function scroll(fn, options) {
|
|
362
|
+
assertCallable("scroll", fn);
|
|
363
|
+
if (options?.metadata === void 0 || options.metadata === null) throw new TypeError("scroll() requires options.metadata.");
|
|
364
|
+
const prop = new PropDescriptor("scroll", fn).merge();
|
|
365
|
+
prop.isScroll = true;
|
|
366
|
+
prop.wrapper = options.wrapper ?? "data";
|
|
367
|
+
prop.scrollMetadata = options.metadata;
|
|
368
|
+
return prop;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Type guard: `true` when `value` is a {@link PropDescriptor}.
|
|
372
|
+
*
|
|
373
|
+
* @param value Value to test.
|
|
374
|
+
*/
|
|
375
|
+
function isPropDescriptor(value) {
|
|
376
|
+
return value instanceof PropDescriptor;
|
|
377
|
+
}
|
|
378
|
+
//#endregion
|
|
379
|
+
//#region src/core/dot-props.ts
|
|
380
|
+
/**
|
|
381
|
+
* Defines `key` on `container` as an own data property, so `__proto__` never reaches the setter.
|
|
382
|
+
*
|
|
383
|
+
* @param container Object to write into.
|
|
384
|
+
* @param key Own key to define.
|
|
385
|
+
* @param value Value to store.
|
|
386
|
+
*/
|
|
387
|
+
function defineOwn(container, key, value) {
|
|
388
|
+
Object.defineProperty(container, key, {
|
|
389
|
+
value,
|
|
390
|
+
enumerable: true,
|
|
391
|
+
writable: true,
|
|
392
|
+
configurable: true
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Calls and awaits `value` when it is a bare function; returns anything else unchanged.
|
|
397
|
+
*
|
|
398
|
+
* @param value Prop value or closure.
|
|
399
|
+
*/
|
|
400
|
+
async function resolveClosure(value) {
|
|
401
|
+
if (typeof value === "function" && !isPropDescriptor(value)) return await value();
|
|
402
|
+
return value;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Shallow copy of `value` when it is a plain object or array, otherwise a fresh empty object.
|
|
406
|
+
*
|
|
407
|
+
* @param value Segment value to copy for writing.
|
|
408
|
+
*/
|
|
409
|
+
function copyForWrite(value) {
|
|
410
|
+
if (Array.isArray(value)) return [...value];
|
|
411
|
+
if (value !== null && typeof value === "object" && !isPropDescriptor(value)) {
|
|
412
|
+
const proto = Object.getPrototypeOf(value);
|
|
413
|
+
if (proto === Object.prototype || proto === null) return { ...value };
|
|
414
|
+
}
|
|
415
|
+
return {};
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Sets `value` at the dot-notation `key` inside `result`, resolving closures along the way.
|
|
419
|
+
*
|
|
420
|
+
* @param result Props object being built; written in place.
|
|
421
|
+
* @param key Dot-notation key.
|
|
422
|
+
* @param value Already-resolved value to set.
|
|
423
|
+
*/
|
|
424
|
+
async function setDotted(result, key, value) {
|
|
425
|
+
const segments = key.split(".");
|
|
426
|
+
const last = segments.pop();
|
|
427
|
+
defineOwn(await segments.reduce(async (parentPromise, segment) => {
|
|
428
|
+
const parent = await parentPromise;
|
|
429
|
+
const next = copyForWrite(await resolveClosure(Object.hasOwn(parent, segment) ? parent[segment] : void 0));
|
|
430
|
+
defineOwn(parent, segment, next);
|
|
431
|
+
return next;
|
|
432
|
+
}, Promise.resolve(result)), last, value);
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Unpacks top-level dot-notation keys into nested objects, like `Arr::set`.
|
|
436
|
+
*
|
|
437
|
+
* Bare functions along the intermediate segments, and at the dotted key itself, are called and
|
|
438
|
+
* awaited first. Non-object segments are replaced by fresh objects. Prop descriptors are left as
|
|
439
|
+
* values for the resolver. The input and the nested objects it holds are never mutated.
|
|
440
|
+
*
|
|
441
|
+
* @param props Page props that may contain dotted keys.
|
|
442
|
+
*/
|
|
443
|
+
async function unpackDotProps(props) {
|
|
444
|
+
return Object.keys(props).filter((key) => key.includes(".")).reduce(async (resultPromise, key) => {
|
|
445
|
+
const result = await resultPromise;
|
|
446
|
+
const value = await resolveClosure(props[key]);
|
|
447
|
+
delete result[key];
|
|
448
|
+
await setDotted(result, key, value);
|
|
449
|
+
return result;
|
|
450
|
+
}, Promise.resolve({ ...props }));
|
|
451
|
+
}
|
|
452
|
+
//#endregion
|
|
453
|
+
//#region src/core/errors.ts
|
|
454
|
+
/**
|
|
455
|
+
* Apply error-bag scoping to a flat errors object, matching the official
|
|
456
|
+
* Inertia behavior. When `bag` is set, errors are namespaced under
|
|
457
|
+
* `props.errors[bag]`. When `bag` is null, errors live at `props.errors`.
|
|
458
|
+
*
|
|
459
|
+
* @param errors Flat field-to-message map.
|
|
460
|
+
* @param bag Optional error-bag name (e.g. `"createUser"`); null for top-level.
|
|
461
|
+
*/
|
|
462
|
+
function scopeErrors(errors, bag) {
|
|
463
|
+
if (bag) return { errors: { [bag]: errors } };
|
|
464
|
+
return { errors };
|
|
465
|
+
}
|
|
466
|
+
//#endregion
|
|
467
|
+
//#region src/core/headers.ts
|
|
468
|
+
/**
|
|
469
|
+
* Canonical Inertia v3 protocol header names.
|
|
470
|
+
*
|
|
471
|
+
* The Inertia client sends these headers on every visit. The server uses
|
|
472
|
+
* them to decide between a full HTML response and a JSON page response,
|
|
473
|
+
* and to scope partial reloads.
|
|
474
|
+
*/
|
|
475
|
+
const INERTIA_HEADERS = {
|
|
476
|
+
inertia: "x-inertia",
|
|
477
|
+
version: "x-inertia-version",
|
|
478
|
+
partialComponent: "x-inertia-partial-component",
|
|
479
|
+
partialData: "x-inertia-partial-data",
|
|
480
|
+
partialExcept: "x-inertia-partial-except",
|
|
481
|
+
errorBag: "x-inertia-error-bag",
|
|
482
|
+
reset: "x-inertia-reset",
|
|
483
|
+
location: "x-inertia-location",
|
|
484
|
+
exceptOnceProps: "x-inertia-except-once-props",
|
|
485
|
+
mergeIntent: "x-inertia-infinite-scroll-merge-intent"
|
|
486
|
+
};
|
|
487
|
+
/**
|
|
488
|
+
* Read a header value as a single string, looking up the lowercase variant as
|
|
489
|
+
* a fallback. Returns the first entry of an array value and `null` when the
|
|
490
|
+
* header is absent.
|
|
491
|
+
*
|
|
492
|
+
* @param headers Raw header record from the HTTP layer.
|
|
493
|
+
* @param name Header name to read.
|
|
494
|
+
*/
|
|
495
|
+
function readHeader(headers, name) {
|
|
496
|
+
const value = headers[name] ?? headers[name.toLowerCase()];
|
|
497
|
+
if (value === void 0 || value === null) return null;
|
|
498
|
+
if (Array.isArray(value)) return value[0] ?? null;
|
|
499
|
+
return value;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Split a comma-separated header value into a trimmed, non-empty string list.
|
|
503
|
+
* Returns `null` when the input is null or yields no entries.
|
|
504
|
+
*
|
|
505
|
+
* @param value Raw header value, or null when absent.
|
|
506
|
+
*/
|
|
507
|
+
function parseList(value) {
|
|
508
|
+
if (value === null) return null;
|
|
509
|
+
const parts = value.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
|
|
510
|
+
return parts.length > 0 ? parts : null;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Parse a request's headers into a normalized {@link InertiaRequestInfo}.
|
|
514
|
+
*
|
|
515
|
+
* @param input Headers, HTTP method, and request URL. Method is uppercased in
|
|
516
|
+
* the returned object; the URL is passed through unchanged.
|
|
517
|
+
*/
|
|
518
|
+
function parseInertiaRequest(input) {
|
|
519
|
+
const { headers } = input;
|
|
520
|
+
const inertia = readHeader(headers, INERTIA_HEADERS.inertia);
|
|
521
|
+
const version = readHeader(headers, INERTIA_HEADERS.version);
|
|
522
|
+
const partialComponent = readHeader(headers, INERTIA_HEADERS.partialComponent);
|
|
523
|
+
const partialOnly = parseList(readHeader(headers, INERTIA_HEADERS.partialData));
|
|
524
|
+
const partialExcept = parseList(readHeader(headers, INERTIA_HEADERS.partialExcept));
|
|
525
|
+
const errorBag = readHeader(headers, INERTIA_HEADERS.errorBag);
|
|
526
|
+
const resetKeys = parseList(readHeader(headers, INERTIA_HEADERS.reset));
|
|
527
|
+
const exceptOnceProps = parseList(readHeader(headers, INERTIA_HEADERS.exceptOnceProps)) ?? [];
|
|
528
|
+
const mergeIntent = readHeader(headers, INERTIA_HEADERS.mergeIntent) === "prepend" ? "prepend" : "append";
|
|
529
|
+
return {
|
|
530
|
+
isInertia: inertia === "true" || inertia === "1",
|
|
531
|
+
version,
|
|
532
|
+
partialComponent,
|
|
533
|
+
partialOnly,
|
|
534
|
+
partialExcept,
|
|
535
|
+
errorBag,
|
|
536
|
+
resetKeys,
|
|
537
|
+
exceptOnceProps,
|
|
538
|
+
mergeIntent,
|
|
539
|
+
method: input.method.toUpperCase(),
|
|
540
|
+
url: input.url
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
//#endregion
|
|
544
|
+
//#region src/core/html.ts
|
|
545
|
+
const HTML_ESCAPES = {
|
|
546
|
+
"&": "&",
|
|
547
|
+
"<": "<",
|
|
548
|
+
">": ">",
|
|
549
|
+
"\"": """,
|
|
550
|
+
"'": "'"
|
|
551
|
+
};
|
|
552
|
+
/**
|
|
553
|
+
* Escape characters that would break out of a double-quoted HTML attribute.
|
|
554
|
+
*
|
|
555
|
+
* @param value Raw attribute value.
|
|
556
|
+
*/
|
|
557
|
+
function escapeHtmlAttribute(value) {
|
|
558
|
+
return value.replace(/[&<>"']/g, (ch) => HTML_ESCAPES[ch] ?? ch);
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Encode an Inertia page object for embedding inside a
|
|
562
|
+
* `<script type="application/json" data-page="...">` tag — the Inertia v3
|
|
563
|
+
* initial-page transport that the client reads via `getInitialPageFromDOM`.
|
|
564
|
+
*
|
|
565
|
+
* Every `<` is replaced with its JSON unicode escape so the payload cannot
|
|
566
|
+
* terminate the surrounding `<\/script>` element or open an HTML comment. The
|
|
567
|
+
* escape is transparent — JSON.parse decodes it back to `<`.
|
|
568
|
+
*
|
|
569
|
+
* @param page Resolved Inertia page object.
|
|
570
|
+
*/
|
|
571
|
+
function encodePageScript(page) {
|
|
572
|
+
return JSON.stringify(page).replace(/</g, "\\u003C");
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Minimal default HTML shell for use when no custom root view is provided.
|
|
576
|
+
* Real apps should plug in a template engine (pug, ejs, react-dom/server,
|
|
577
|
+
* etc.) and embed `encodePageScript(page)` themselves.
|
|
578
|
+
*
|
|
579
|
+
* The initial page object is emitted as a
|
|
580
|
+
* `<script type="application/json" data-page="...">` tag, the Inertia v3
|
|
581
|
+
* transport that the client reads on boot.
|
|
582
|
+
*
|
|
583
|
+
* @param input Page object plus optional title/head/rootId/scriptSrc/SSR markup
|
|
584
|
+
* — see {@link DefaultRootViewInput}.
|
|
585
|
+
*/
|
|
586
|
+
function renderDefaultHtml(input) {
|
|
587
|
+
const rootId = input.rootId ?? "app";
|
|
588
|
+
const title = input.title ?? "Inertia App";
|
|
589
|
+
const head = input.head ?? "";
|
|
590
|
+
const script = input.scriptSrc ? `<script type="module" src="${escapeHtmlAttribute(input.scriptSrc)}"><\/script>` : "";
|
|
591
|
+
let rootMarkup;
|
|
592
|
+
if (input.ssrFull) rootMarkup = input.ssrFull;
|
|
593
|
+
else {
|
|
594
|
+
const idAttr = escapeHtmlAttribute(rootId);
|
|
595
|
+
rootMarkup = `<div id="${idAttr}">${input.ssrBody ?? ""}</div>\n ${`<script data-page="${idAttr}" type="application/json">${encodePageScript(input.page)}<\/script>`}`;
|
|
596
|
+
}
|
|
597
|
+
return `<!DOCTYPE html>
|
|
598
|
+
<html>
|
|
599
|
+
<head>
|
|
600
|
+
<meta charset="utf-8" />
|
|
601
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
602
|
+
<title>${escapeHtmlAttribute(title)}</title>
|
|
603
|
+
${head}
|
|
604
|
+
</head>
|
|
605
|
+
<body>
|
|
606
|
+
${rootMarkup}
|
|
607
|
+
${script}
|
|
608
|
+
</body>
|
|
609
|
+
</html>`;
|
|
610
|
+
}
|
|
611
|
+
/** Default user-facing messages keyed by HTTP status, used when none is given. */
|
|
612
|
+
const ERROR_STATUS_MESSAGES = {
|
|
613
|
+
400: "Bad Request",
|
|
614
|
+
401: "Unauthorized",
|
|
615
|
+
403: "Forbidden",
|
|
616
|
+
404: "Page Not Found",
|
|
617
|
+
409: "Conflict",
|
|
618
|
+
419: "Page Expired",
|
|
619
|
+
422: "Unprocessable Entity",
|
|
620
|
+
429: "Too Many Requests",
|
|
621
|
+
500: "Server Error",
|
|
622
|
+
502: "Bad Gateway",
|
|
623
|
+
503: "Service Unavailable",
|
|
624
|
+
504: "Gateway Timeout"
|
|
625
|
+
};
|
|
626
|
+
/**
|
|
627
|
+
* Render a minimal, SPA-less standalone HTML error page. Used as the hard
|
|
628
|
+
* fallback when no client `Error` component can be resolved (e.g. a plain
|
|
629
|
+
* browser load, or an SSR/render failure on the Inertia path), so a user
|
|
630
|
+
* never receives a raw JSON error or a blank page.
|
|
631
|
+
*
|
|
632
|
+
* @param input HTTP status plus an optional override message — see {@link ErrorPageInput}.
|
|
633
|
+
*/
|
|
634
|
+
function renderErrorPage(input) {
|
|
635
|
+
const { status } = input;
|
|
636
|
+
const message = input.message ?? ERROR_STATUS_MESSAGES[status] ?? "Something went wrong";
|
|
637
|
+
const safeStatus = escapeHtmlAttribute(String(status));
|
|
638
|
+
const safeMessage = escapeHtmlAttribute(message);
|
|
639
|
+
return `<!DOCTYPE html>
|
|
640
|
+
<html>
|
|
641
|
+
<head>
|
|
642
|
+
<meta charset="utf-8" />
|
|
643
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
644
|
+
<title>${safeStatus} ${safeMessage}</title>
|
|
645
|
+
<style>
|
|
646
|
+
body { font-family: system-ui, sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f8f9fa; color: #212529; }
|
|
647
|
+
main { text-align: center; padding: 2rem; }
|
|
648
|
+
h1 { font-size: 4rem; margin: 0; font-weight: 600; }
|
|
649
|
+
p { font-size: 1.25rem; margin: 0.5rem 0 0; color: #6c757d; }
|
|
650
|
+
</style>
|
|
651
|
+
</head>
|
|
652
|
+
<body>
|
|
653
|
+
<main>
|
|
654
|
+
<h1>${safeStatus}</h1>
|
|
655
|
+
<p>${safeMessage}</p>
|
|
656
|
+
</main>
|
|
657
|
+
</body>
|
|
658
|
+
</html>`;
|
|
659
|
+
}
|
|
660
|
+
//#endregion
|
|
661
|
+
//#region src/core/page.ts
|
|
662
|
+
/**
|
|
663
|
+
* Whether flash data is worth emitting as a top-level page key. Nullish and
|
|
664
|
+
* empty-object flash are dropped so the wire payload stays compact and
|
|
665
|
+
* absent flash never surfaces a stray `flash: {}` to the client.
|
|
666
|
+
*
|
|
667
|
+
* @param flash Candidate flash value pulled from the session.
|
|
668
|
+
*/
|
|
669
|
+
function hasFlashData(flash) {
|
|
670
|
+
if (flash === null || flash === void 0) return false;
|
|
671
|
+
if (typeof flash === "object" && !Array.isArray(flash)) return Object.keys(flash).length > 0;
|
|
672
|
+
return true;
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Every {@link PropsMetadata} key, in protocol order. `createPage` copies only
|
|
676
|
+
* these keys, so a widened metadata object cannot overwrite page fields.
|
|
677
|
+
*/
|
|
678
|
+
const METADATA_KEYS = [
|
|
679
|
+
"sharedProps",
|
|
680
|
+
"mergeProps",
|
|
681
|
+
"prependProps",
|
|
682
|
+
"deepMergeProps",
|
|
683
|
+
"matchPropsOn",
|
|
684
|
+
"deferredProps",
|
|
685
|
+
"rescuedProps",
|
|
686
|
+
"scrollProps",
|
|
687
|
+
"onceProps"
|
|
688
|
+
];
|
|
689
|
+
/**
|
|
690
|
+
* Whether a metadata value carries anything worth emitting: a non-empty array
|
|
691
|
+
* or an object with at least one key.
|
|
692
|
+
*
|
|
693
|
+
* @param value Candidate metadata value from {@link PropsMetadata}.
|
|
694
|
+
*/
|
|
695
|
+
function hasMetadataEntries(value) {
|
|
696
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
697
|
+
if (value !== null && typeof value === "object") return Object.keys(value).length > 0;
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Build a normalized Inertia v3 page object. `clearHistory`, `encryptHistory`
|
|
702
|
+
* and `preserveFragment` default to `false` and are always present. Each
|
|
703
|
+
* `metadata` key is copied onto the page only when non-empty so the JSON over
|
|
704
|
+
* the wire stays compact. `flash` is emitted as a top-level key only when it
|
|
705
|
+
* carries data (non-nullish, and a non-empty object when object-valued) —
|
|
706
|
+
* mirroring inertia-laravel's `$flash ? ['flash' => $flash] : []`.
|
|
707
|
+
*
|
|
708
|
+
* @param input Required page fields plus the optional v3 extras.
|
|
709
|
+
*/
|
|
710
|
+
function createPage(input) {
|
|
711
|
+
const page = {
|
|
712
|
+
component: input.component,
|
|
713
|
+
props: input.props,
|
|
714
|
+
url: input.url,
|
|
715
|
+
version: input.version,
|
|
716
|
+
clearHistory: input.clearHistory ?? false,
|
|
717
|
+
encryptHistory: input.encryptHistory ?? false,
|
|
718
|
+
preserveFragment: input.preserveFragment ?? false
|
|
719
|
+
};
|
|
720
|
+
const { metadata } = input;
|
|
721
|
+
if (metadata) METADATA_KEYS.forEach((key) => {
|
|
722
|
+
if (hasMetadataEntries(metadata[key])) Object.assign(page, { [key]: metadata[key] });
|
|
723
|
+
});
|
|
724
|
+
if (hasFlashData(input.flash)) page.flash = input.flash;
|
|
725
|
+
return page;
|
|
726
|
+
}
|
|
727
|
+
//#endregion
|
|
728
|
+
//#region src/core/partial-filter.ts
|
|
729
|
+
/**
|
|
730
|
+
* `true` when `path` equals `parent` or lies in its dot-notation subtree.
|
|
731
|
+
*
|
|
732
|
+
* @param path Dot-notation prop path to test.
|
|
733
|
+
* @param parent Dot-notation path that may contain `path`.
|
|
734
|
+
*/
|
|
735
|
+
function pathIsSameOrDescendant(path, parent) {
|
|
736
|
+
return path === parent || path.startsWith(`${parent}.`);
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* `true` when `path` matches an `only` entry or is a descendant of one.
|
|
740
|
+
*
|
|
741
|
+
* @param path Dot-notation prop path to test.
|
|
742
|
+
* @param only Requested partial-reload `only` paths.
|
|
743
|
+
*/
|
|
744
|
+
function matchesOnly(path, only) {
|
|
745
|
+
return only.some((onlyPath) => pathIsSameOrDescendant(path, onlyPath));
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* `true` when `path` is a strict ancestor of an `only` entry.
|
|
749
|
+
*
|
|
750
|
+
* @param path Dot-notation prop path to test.
|
|
751
|
+
* @param only Requested partial-reload `only` paths.
|
|
752
|
+
*/
|
|
753
|
+
function leadsToOnly(path, only) {
|
|
754
|
+
return only.some((onlyPath) => onlyPath.startsWith(`${path}.`));
|
|
755
|
+
}
|
|
756
|
+
/**
|
|
757
|
+
* `true` when `path` equals an `except` entry, is a descendant of one, or is
|
|
758
|
+
* an ancestor of one. Matching ancestors mirrors the reference Node server,
|
|
759
|
+
* whose flat filter drops a top-level key whenever any `except` entry lives
|
|
760
|
+
* under it; inertia-laravel keeps the ancestor and drops only the child.
|
|
761
|
+
*
|
|
762
|
+
* @param path Dot-notation prop path to test.
|
|
763
|
+
* @param except Requested partial-reload `except` paths.
|
|
764
|
+
*/
|
|
765
|
+
function matchesExcept(path, except) {
|
|
766
|
+
return except.some((exceptPath) => pathIsSameOrDescendant(path, exceptPath) || pathIsSameOrDescendant(exceptPath, path));
|
|
767
|
+
}
|
|
768
|
+
//#endregion
|
|
769
|
+
//#region src/core/props-resolver.ts
|
|
770
|
+
/**
|
|
771
|
+
* `true` for arrays and for objects whose prototype is `Object.prototype` or `null`.
|
|
772
|
+
*
|
|
773
|
+
* @param value Value to test.
|
|
774
|
+
*/
|
|
775
|
+
function isPlainObjectOrArray(value) {
|
|
776
|
+
if (Array.isArray(value)) return true;
|
|
777
|
+
if (value === null || typeof value !== "object") return false;
|
|
778
|
+
const proto = Object.getPrototypeOf(value);
|
|
779
|
+
return proto === Object.prototype || proto === null;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Normalize a partial header list: empty entries are dropped and an empty list becomes `null`.
|
|
783
|
+
*
|
|
784
|
+
* @param list Parsed header list, or `null` when absent.
|
|
785
|
+
*/
|
|
786
|
+
function normalizeList(list) {
|
|
787
|
+
const entries = (list ?? []).filter((entry) => entry !== "");
|
|
788
|
+
return entries.length > 0 ? entries : null;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Default rescue hook: log the rescued error with the prop's path.
|
|
792
|
+
*
|
|
793
|
+
* @param error The thrown value.
|
|
794
|
+
* @param path Dot path of the rescued prop.
|
|
795
|
+
*/
|
|
796
|
+
function reportRescuedError(error, path) {
|
|
797
|
+
console.error(`Inertia prop '${path}' failed to resolve and was rescued.`, error);
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Report a failure of a custom rescue hook through the default reporter:
|
|
801
|
+
* the original rescued error, then the hook's own error.
|
|
802
|
+
*
|
|
803
|
+
* @param hookError The value the hook threw or rejected with.
|
|
804
|
+
* @param error The rescued error the hook was given.
|
|
805
|
+
* @param path Dot path of the rescued prop.
|
|
806
|
+
*/
|
|
807
|
+
function reportHookFailure(hookError, error, path) {
|
|
808
|
+
reportRescuedError(error, path);
|
|
809
|
+
console.error(`Inertia onRescue hook failed while reporting prop '${path}'.`, hookError);
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Marker returned by a settled entry that is left out of its parent node.
|
|
813
|
+
*/
|
|
814
|
+
const EXCLUDED = Symbol("excluded");
|
|
815
|
+
/**
|
|
816
|
+
* Every {@link Rescued} wrapper created by {@link rescued}.
|
|
817
|
+
*/
|
|
818
|
+
const rescuedMarkers = /* @__PURE__ */ new WeakSet();
|
|
819
|
+
/**
|
|
820
|
+
* Wrap a rescued error.
|
|
821
|
+
*
|
|
822
|
+
* @param error The thrown value.
|
|
823
|
+
*/
|
|
824
|
+
function rescued(error) {
|
|
825
|
+
const marker = { error };
|
|
826
|
+
rescuedMarkers.add(marker);
|
|
827
|
+
return marker;
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* `true` when `value` is a wrapper created by {@link rescued}.
|
|
831
|
+
*
|
|
832
|
+
* @param value A resolved value.
|
|
833
|
+
*/
|
|
834
|
+
function isRescued(value) {
|
|
835
|
+
return typeof value === "object" && value !== null && rescuedMarkers.has(value);
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* `true` when a prop must be resolved asynchronously: a descriptor, a
|
|
839
|
+
* function, or a thenable. Anything else is copied as is.
|
|
840
|
+
*
|
|
841
|
+
* @param prop The prop as found in its parent node.
|
|
842
|
+
*/
|
|
843
|
+
function needsResolution(prop) {
|
|
844
|
+
if (typeof prop === "function") return true;
|
|
845
|
+
if (prop === null || typeof prop !== "object") return false;
|
|
846
|
+
return isPropDescriptor(prop) || typeof prop.then === "function";
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Swallow a rejection on a started resolution that may never be awaited
|
|
850
|
+
* because an earlier sibling failed; the awaited copy still rejects.
|
|
851
|
+
*/
|
|
852
|
+
function ignoreRejection() {}
|
|
853
|
+
/**
|
|
854
|
+
* Write a settled entry into the node being built, skipping excluded entries.
|
|
855
|
+
* Object keys are written as own data properties, so `__proto__` never
|
|
856
|
+
* reaches the prototype setter.
|
|
857
|
+
*
|
|
858
|
+
* @param result Node being built; written in place.
|
|
859
|
+
* @param key Key of the entry.
|
|
860
|
+
* @param outcome Settled value, or {@link EXCLUDED}.
|
|
861
|
+
*/
|
|
862
|
+
function writeEntry(result, key, outcome) {
|
|
863
|
+
if (outcome === EXCLUDED) return;
|
|
864
|
+
if (Array.isArray(result)) result.push(outcome);
|
|
865
|
+
else if (key === "__proto__") Object.defineProperty(result, key, {
|
|
866
|
+
value: outcome,
|
|
867
|
+
enumerable: true,
|
|
868
|
+
writable: true,
|
|
869
|
+
configurable: true
|
|
870
|
+
});
|
|
871
|
+
else result[key] = outcome;
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Resolves shared and page props for one response, applying partial-reload
|
|
875
|
+
* filtering and initial-load exclusion and collecting page metadata. Port of
|
|
876
|
+
* inertia-laravel's `PropsResolver`. Use one instance per response: the
|
|
877
|
+
* instance accumulates metadata, so {@link PropsResolver.resolve} throws when
|
|
878
|
+
* called a second time. The resolver only reads descriptors, never writes
|
|
879
|
+
* them. Metadata maps keyed by user input (`deferredProps`, `scrollProps`,
|
|
880
|
+
* `onceProps`) have a null prototype, so any key, `__proto__` included, is
|
|
881
|
+
* stored as an own property.
|
|
882
|
+
*/
|
|
883
|
+
var PropsResolver = class {
|
|
884
|
+
request;
|
|
885
|
+
component;
|
|
886
|
+
onRescue;
|
|
887
|
+
isPartial;
|
|
888
|
+
isInertia;
|
|
889
|
+
only;
|
|
890
|
+
except;
|
|
891
|
+
sharedPropKeys = [];
|
|
892
|
+
mergeProps = [];
|
|
893
|
+
prependProps = [];
|
|
894
|
+
deepMergeProps = [];
|
|
895
|
+
matchPropsOn = [];
|
|
896
|
+
deferredProps = Object.create(null);
|
|
897
|
+
rescuedProps = [];
|
|
898
|
+
scrollProps = Object.create(null);
|
|
899
|
+
onceProps = Object.create(null);
|
|
900
|
+
used = false;
|
|
901
|
+
/**
|
|
902
|
+
* Create a resolver for one request and component.
|
|
903
|
+
*
|
|
904
|
+
* @param request Normalized Inertia request state.
|
|
905
|
+
* @param component Component being rendered.
|
|
906
|
+
* @param onRescue Hook called when a rescued prop throws.
|
|
907
|
+
*/
|
|
908
|
+
constructor(request, component, onRescue) {
|
|
909
|
+
this.request = request;
|
|
910
|
+
this.component = component;
|
|
911
|
+
this.onRescue = onRescue ?? reportRescuedError;
|
|
912
|
+
this.isPartial = request.partialComponent === component;
|
|
913
|
+
this.isInertia = request.isInertia;
|
|
914
|
+
this.only = normalizeList(request.partialOnly);
|
|
915
|
+
this.except = normalizeList(request.partialExcept);
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Resolve the given shared and page props, collecting their metadata.
|
|
919
|
+
*
|
|
920
|
+
* @param shared Already-evaluated shared props.
|
|
921
|
+
* @param props Page props; these win over shared props on collision.
|
|
922
|
+
*/
|
|
923
|
+
async resolve(shared, props) {
|
|
924
|
+
if (this.used) throw new Error("PropsResolver.resolve() was already called on this instance; create one resolver per response.");
|
|
925
|
+
this.used = true;
|
|
926
|
+
const combined = {
|
|
927
|
+
...this.resolveSharedProps(shared),
|
|
928
|
+
...props
|
|
929
|
+
};
|
|
930
|
+
return {
|
|
931
|
+
props: await this.walk(await unpackDotProps(combined), "", false),
|
|
932
|
+
metadata: this.buildMetadata()
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Record the top-level keys of shared props (the segment before the first
|
|
937
|
+
* `.` for dot-notation keys), deduplicated in first-seen order.
|
|
938
|
+
*
|
|
939
|
+
* @param shared Already-evaluated shared props.
|
|
940
|
+
*/
|
|
941
|
+
resolveSharedProps(shared) {
|
|
942
|
+
Object.keys(shared).forEach((key) => {
|
|
943
|
+
const [topLevel = key] = key.split(".", 1);
|
|
944
|
+
if (!this.sharedPropKeys.includes(topLevel)) this.sharedPropKeys.push(topLevel);
|
|
945
|
+
});
|
|
946
|
+
return shared;
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Build the non-empty metadata entries for the page response, in protocol order.
|
|
950
|
+
*/
|
|
951
|
+
buildMetadata() {
|
|
952
|
+
const entries = [
|
|
953
|
+
["sharedProps", this.sharedPropKeys],
|
|
954
|
+
["mergeProps", this.mergeProps],
|
|
955
|
+
["prependProps", this.prependProps],
|
|
956
|
+
["deepMergeProps", this.deepMergeProps],
|
|
957
|
+
["matchPropsOn", this.matchPropsOn],
|
|
958
|
+
["deferredProps", this.deferredProps],
|
|
959
|
+
["rescuedProps", this.rescuedProps],
|
|
960
|
+
["scrollProps", this.scrollProps],
|
|
961
|
+
["onceProps", this.onceProps]
|
|
962
|
+
];
|
|
963
|
+
return Object.fromEntries(entries.filter(([, value]) => Object.keys(value).length > 0));
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Recursively resolve a props node, collecting metadata along the way.
|
|
967
|
+
* Every included entry that needs resolving is started in insertion order
|
|
968
|
+
* before any is awaited, so siblings resolve concurrently; each entry is
|
|
969
|
+
* then settled in insertion order (rescue, one-level unwrap, metadata,
|
|
970
|
+
* recursion), keeping output key order and metadata order deterministic.
|
|
971
|
+
* Plain values are copied synchronously, so a node without closures or
|
|
972
|
+
* descriptors comes back without a promise. Arrays come back as arrays
|
|
973
|
+
* with excluded entries dropped, so later index-based `except`/`reset`
|
|
974
|
+
* paths shift down; objects come back as fresh plain objects. When the
|
|
975
|
+
* original prop was not itself a plain object or array (e.g. a closure
|
|
976
|
+
* that returned one), its children bypass partial filtering.
|
|
977
|
+
*
|
|
978
|
+
* @param node Plain object or array to walk.
|
|
979
|
+
* @param prefix Dot path of `node`, or `''` at the root.
|
|
980
|
+
* @param parentWasResolved Whether an ancestor was produced by resolving a closure or descriptor.
|
|
981
|
+
*/
|
|
982
|
+
walk(node, prefix, parentWasResolved) {
|
|
983
|
+
const entries = this.startEntries(node, prefix, parentWasResolved);
|
|
984
|
+
const result = Array.isArray(node) ? [] : {};
|
|
985
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
986
|
+
const entry = entries[index];
|
|
987
|
+
const outcome = this.settleEntry(entry, parentWasResolved);
|
|
988
|
+
if (outcome instanceof Promise) return this.finishWalk(entries, index, outcome, result, parentWasResolved);
|
|
989
|
+
writeEntry(result, entry.key, outcome);
|
|
990
|
+
}
|
|
991
|
+
return result;
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Continue a walk asynchronously from the first entry whose outcome is pending.
|
|
995
|
+
*
|
|
996
|
+
* @param entries Started entries of the node.
|
|
997
|
+
* @param index Index of the pending entry.
|
|
998
|
+
* @param pending Outcome of the entry at `index`.
|
|
999
|
+
* @param result Node being built; written in place.
|
|
1000
|
+
* @param parentWasResolved Whether an ancestor was produced by resolving a closure or descriptor.
|
|
1001
|
+
*/
|
|
1002
|
+
async finishWalk(entries, index, pending, result, parentWasResolved) {
|
|
1003
|
+
writeEntry(result, entries[index].key, await pending);
|
|
1004
|
+
for (let next = index + 1; next < entries.length; next += 1) {
|
|
1005
|
+
const entry = entries[next];
|
|
1006
|
+
const outcome = this.settleEntry(entry, parentWasResolved);
|
|
1007
|
+
writeEntry(result, entry.key, outcome instanceof Promise ? await outcome : outcome);
|
|
1008
|
+
}
|
|
1009
|
+
return result;
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Run the synchronous pre-resolve checks (partial filter, initial-load
|
|
1013
|
+
* exclusion) over a node's entries in insertion order and start resolving
|
|
1014
|
+
* every included entry that needs it.
|
|
1015
|
+
*
|
|
1016
|
+
* @param node Plain object or array to walk.
|
|
1017
|
+
* @param prefix Dot path of `node`, or `''` at the root.
|
|
1018
|
+
* @param parentWasResolved Whether an ancestor was produced by resolving a closure or descriptor.
|
|
1019
|
+
*/
|
|
1020
|
+
startEntries(node, prefix, parentWasResolved) {
|
|
1021
|
+
const entries = [];
|
|
1022
|
+
const visit = (prop, key) => {
|
|
1023
|
+
const path = prefix === "" ? key : `${prefix}.${key}`;
|
|
1024
|
+
if (this.shouldIncludeInPartialResponse(prop, path, parentWasResolved)) entries.push(this.startEntry(key, path, prop));
|
|
1025
|
+
};
|
|
1026
|
+
if (Array.isArray(node)) node.forEach((prop, index) => visit(prop, String(index)));
|
|
1027
|
+
else Object.keys(node).forEach((key) => visit(node[key], key));
|
|
1028
|
+
return entries;
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Classify one included entry: excluded from the initial load, pending
|
|
1032
|
+
* resolution (started now), or a plain value copied as is.
|
|
1033
|
+
*
|
|
1034
|
+
* @param key Key of the entry in its parent node.
|
|
1035
|
+
* @param path Dot path of the entry.
|
|
1036
|
+
* @param prop The prop as found in its parent node.
|
|
1037
|
+
*/
|
|
1038
|
+
startEntry(key, path, prop) {
|
|
1039
|
+
if (!this.isPartial && this.isExcludedFromInitialResponse(prop, path)) return {
|
|
1040
|
+
key,
|
|
1041
|
+
path,
|
|
1042
|
+
prop,
|
|
1043
|
+
excluded: true,
|
|
1044
|
+
pending: null
|
|
1045
|
+
};
|
|
1046
|
+
if (needsResolution(prop)) {
|
|
1047
|
+
const pending = this.resolveValue(prop);
|
|
1048
|
+
pending.catch(ignoreRejection);
|
|
1049
|
+
return {
|
|
1050
|
+
key,
|
|
1051
|
+
path,
|
|
1052
|
+
prop,
|
|
1053
|
+
excluded: false,
|
|
1054
|
+
pending
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
return {
|
|
1058
|
+
key,
|
|
1059
|
+
path,
|
|
1060
|
+
prop,
|
|
1061
|
+
excluded: false,
|
|
1062
|
+
pending: null
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Finish one started entry in insertion order. Returns the value to write,
|
|
1067
|
+
* {@link EXCLUDED}, or a promise of either when the entry needs awaiting.
|
|
1068
|
+
*
|
|
1069
|
+
* @param entry The started entry.
|
|
1070
|
+
* @param parentWasResolved Whether an ancestor was produced by resolving a closure or descriptor.
|
|
1071
|
+
*/
|
|
1072
|
+
settleEntry(entry, parentWasResolved) {
|
|
1073
|
+
if (entry.excluded) {
|
|
1074
|
+
this.collectExclusionMetadata(entry.prop, entry.path);
|
|
1075
|
+
return EXCLUDED;
|
|
1076
|
+
}
|
|
1077
|
+
if (entry.pending !== null) return this.settleResolved(entry, entry.pending, parentWasResolved);
|
|
1078
|
+
return this.include(entry.prop, entry.prop, entry.path, parentWasResolved);
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Await a started resolution, then apply rescue, the one-level descriptor
|
|
1082
|
+
* unwrap (with its own initial-load exclusion) and inclusion.
|
|
1083
|
+
*
|
|
1084
|
+
* @param entry The started entry.
|
|
1085
|
+
* @param pending The entry's resolution.
|
|
1086
|
+
* @param parentWasResolved Whether an ancestor was produced by resolving a closure or descriptor.
|
|
1087
|
+
*/
|
|
1088
|
+
async settleResolved(entry, pending, parentWasResolved) {
|
|
1089
|
+
const { path } = entry;
|
|
1090
|
+
let current = entry.prop;
|
|
1091
|
+
let value = await pending;
|
|
1092
|
+
if (isRescued(value)) return this.rescue(value.error, path);
|
|
1093
|
+
if (value !== current && this.isPropType(value)) {
|
|
1094
|
+
current = value;
|
|
1095
|
+
if (!this.isPartial && this.isExcludedFromInitialResponse(current, path)) {
|
|
1096
|
+
this.collectExclusionMetadata(current, path);
|
|
1097
|
+
return EXCLUDED;
|
|
1098
|
+
}
|
|
1099
|
+
value = await this.resolveValue(current);
|
|
1100
|
+
if (isRescued(value)) return this.rescue(value.error, path);
|
|
1101
|
+
}
|
|
1102
|
+
return this.include(current, value, path, parentWasResolved);
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Include a resolved prop: check scroll shape, collect metadata, and walk
|
|
1106
|
+
* into plain objects and arrays.
|
|
1107
|
+
*
|
|
1108
|
+
* @param prop The prop that produced `value`.
|
|
1109
|
+
* @param value The prop's resolved value.
|
|
1110
|
+
* @param path Dot path of the prop.
|
|
1111
|
+
* @param parentWasResolved Whether an ancestor was produced by resolving a closure or descriptor.
|
|
1112
|
+
*/
|
|
1113
|
+
include(prop, value, path, parentWasResolved) {
|
|
1114
|
+
this.assertScrollShape(prop, path, value);
|
|
1115
|
+
this.collectMetadata(prop, path, value);
|
|
1116
|
+
if (isPlainObjectOrArray(value)) return this.walk(value, path, parentWasResolved || !isPlainObjectOrArray(prop));
|
|
1117
|
+
return value;
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Determine if a prop belongs in the response: always on a full visit;
|
|
1121
|
+
* on a partial reload, when it bypasses filtering or its path matches the request.
|
|
1122
|
+
*
|
|
1123
|
+
* @param prop The prop being considered.
|
|
1124
|
+
* @param path Dot path of the prop.
|
|
1125
|
+
* @param parentWasResolved Whether an ancestor was produced by resolving a closure or descriptor.
|
|
1126
|
+
*/
|
|
1127
|
+
shouldIncludeInPartialResponse(prop, path, parentWasResolved) {
|
|
1128
|
+
if (!this.isPartial || this.bypassesPartialFiltering(prop, parentWasResolved)) return true;
|
|
1129
|
+
return this.pathMatchesPartialRequest(path);
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Determine if a prop is resolved independently of partial filters:
|
|
1133
|
+
* always props, and children of already-resolved values.
|
|
1134
|
+
*
|
|
1135
|
+
* @param prop The prop being considered.
|
|
1136
|
+
* @param parentWasResolved Whether an ancestor was produced by resolving a closure or descriptor.
|
|
1137
|
+
*/
|
|
1138
|
+
bypassesPartialFiltering(prop, parentWasResolved) {
|
|
1139
|
+
return isPropDescriptor(prop) && prop.isAlways || parentWasResolved;
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Determine if the path matches the partial request using bidirectional
|
|
1143
|
+
* prefix matching on both the only list and the except list.
|
|
1144
|
+
*
|
|
1145
|
+
* @param path Dot path of the prop.
|
|
1146
|
+
*/
|
|
1147
|
+
pathMatchesPartialRequest(path) {
|
|
1148
|
+
if (this.only !== null && !matchesOnly(path, this.only) && !leadsToOnly(path, this.only)) return false;
|
|
1149
|
+
if (this.except !== null && matchesExcept(path, this.except)) return false;
|
|
1150
|
+
return true;
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Determine if a prop is excluded from the initial page response: an
|
|
1154
|
+
* optional or deferred descriptor, or a once-prop the client already holds.
|
|
1155
|
+
*
|
|
1156
|
+
* @param prop The prop being considered.
|
|
1157
|
+
* @param path Dot path of the prop.
|
|
1158
|
+
*/
|
|
1159
|
+
isExcludedFromInitialResponse(prop, path) {
|
|
1160
|
+
if (!isPropDescriptor(prop)) return false;
|
|
1161
|
+
return prop.ignoreFirstLoad || prop.defers || this.isInertia && this.wasAlreadyLoadedByClient(prop, path);
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Collect the metadata of a prop excluded from the initial response
|
|
1165
|
+
* according to why it was excluded.
|
|
1166
|
+
*
|
|
1167
|
+
* @param prop The descriptor being excluded.
|
|
1168
|
+
* @param path Dot path of the prop.
|
|
1169
|
+
*/
|
|
1170
|
+
collectExclusionMetadata(prop, path) {
|
|
1171
|
+
if (prop.ignoreFirstLoad) this.collectIgnoredPropMetadata(prop, path);
|
|
1172
|
+
else if (prop.defers) this.collectDeferredExclusionMetadata(prop, path);
|
|
1173
|
+
else this.collectOnceMetadata(path, prop);
|
|
1174
|
+
}
|
|
1175
|
+
/**
|
|
1176
|
+
* Collect the deferred, merge and once metadata of an excluded
|
|
1177
|
+
* first-load-ignored prop (optional, defer).
|
|
1178
|
+
*
|
|
1179
|
+
* @param prop The descriptor being excluded.
|
|
1180
|
+
* @param path Dot path of the prop.
|
|
1181
|
+
*/
|
|
1182
|
+
collectIgnoredPropMetadata(prop, path) {
|
|
1183
|
+
if (prop.defers && !this.wasAlreadyLoadedByClient(prop, path)) this.collectDeferredPropMetadata(path, prop);
|
|
1184
|
+
if (prop.merges) this.collectMergeableMetadata(path, prop, true);
|
|
1185
|
+
if (prop.resolvesOnce) this.collectOnceMetadata(path, prop);
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Collect the deferred-group and merge metadata of an excluded deferred prop.
|
|
1189
|
+
*
|
|
1190
|
+
* @param prop The descriptor being excluded.
|
|
1191
|
+
* @param path Dot path of the prop.
|
|
1192
|
+
*/
|
|
1193
|
+
collectDeferredExclusionMetadata(prop, path) {
|
|
1194
|
+
this.collectDeferredPropMetadata(path, prop);
|
|
1195
|
+
if (prop.merges) this.collectMergeableMetadata(path, prop, true);
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Determine if a once-prop has already been loaded by the client: it
|
|
1199
|
+
* resolves once, is not forced fresh, and its key (or path) is listed in
|
|
1200
|
+
* the client's except-once header.
|
|
1201
|
+
*
|
|
1202
|
+
* @param prop The descriptor being considered.
|
|
1203
|
+
* @param path Dot path of the prop.
|
|
1204
|
+
*/
|
|
1205
|
+
wasAlreadyLoadedByClient(prop, path) {
|
|
1206
|
+
return prop.resolvesOnce && !prop.isFresh && this.request.exceptOnceProps.includes(prop.onceKey ?? path);
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Resolve a single prop value: a descriptor resolves its source, a bare
|
|
1210
|
+
* function is called, and the result is awaited; anything else passes
|
|
1211
|
+
* through. An error thrown by a rescuable descriptor comes back wrapped in
|
|
1212
|
+
* {@link Rescued}; any other error rejects.
|
|
1213
|
+
*
|
|
1214
|
+
* @param prop The prop to resolve.
|
|
1215
|
+
*/
|
|
1216
|
+
async resolveValue(prop) {
|
|
1217
|
+
try {
|
|
1218
|
+
if (isPropDescriptor(prop)) return await prop.resolveSource();
|
|
1219
|
+
if (typeof prop === "function") return await prop();
|
|
1220
|
+
return await prop;
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
if (isPropDescriptor(prop) && prop.rescues) return rescued(error);
|
|
1223
|
+
throw error;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
/**
|
|
1227
|
+
* Throw when a scroll prop did not resolve to a plain object holding its
|
|
1228
|
+
* wrapper key as an own property.
|
|
1229
|
+
*
|
|
1230
|
+
* @param prop The prop being included.
|
|
1231
|
+
* @param path Dot path of the prop.
|
|
1232
|
+
* @param resolved The prop's resolved value.
|
|
1233
|
+
*/
|
|
1234
|
+
assertScrollShape(prop, path, resolved) {
|
|
1235
|
+
if (!isPropDescriptor(prop) || !prop.isScroll) return;
|
|
1236
|
+
if (Array.isArray(resolved) || !isPlainObjectOrArray(resolved) || !Object.hasOwn(resolved, prop.wrapper)) throw new TypeError(`Scroll prop '${path}' must resolve to an object with a '${prop.wrapper}' key`);
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Record a rescued prop: report the error, add its path to `rescuedProps`
|
|
1240
|
+
* and omit it from the response.
|
|
1241
|
+
*
|
|
1242
|
+
* @param error The thrown value.
|
|
1243
|
+
* @param path Dot path of the prop.
|
|
1244
|
+
*/
|
|
1245
|
+
rescue(error, path) {
|
|
1246
|
+
this.reportRescue(error, path);
|
|
1247
|
+
this.rescuedProps.push(path);
|
|
1248
|
+
return EXCLUDED;
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* Call the rescue hook with the failure contained: a synchronous throw or
|
|
1252
|
+
* a rejected returned thenable is logged through the default reporter and
|
|
1253
|
+
* never propagates or surfaces as an unhandled rejection.
|
|
1254
|
+
*
|
|
1255
|
+
* @param error The thrown value.
|
|
1256
|
+
* @param path Dot path of the prop.
|
|
1257
|
+
*/
|
|
1258
|
+
reportRescue(error, path) {
|
|
1259
|
+
const hook = this.onRescue;
|
|
1260
|
+
try {
|
|
1261
|
+
const result = hook(error, path);
|
|
1262
|
+
if (result !== null && typeof result === "object" && typeof result.then === "function") Promise.resolve(result).catch((hookError) => reportHookFailure(hookError, error, path));
|
|
1263
|
+
} catch (hookError) {
|
|
1264
|
+
reportHookFailure(hookError, error, path);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Determine if the value is a prop type that needs further filtering or
|
|
1269
|
+
* metadata collection.
|
|
1270
|
+
*
|
|
1271
|
+
* @param value The resolved value.
|
|
1272
|
+
*/
|
|
1273
|
+
isPropType(value) {
|
|
1274
|
+
return isPropDescriptor(value);
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Collect metadata for a prop that will be included in the response.
|
|
1278
|
+
*
|
|
1279
|
+
* @param prop The prop being included.
|
|
1280
|
+
* @param path Dot path of the prop.
|
|
1281
|
+
* @param resolved The prop's resolved value.
|
|
1282
|
+
*/
|
|
1283
|
+
collectMetadata(prop, path, resolved) {
|
|
1284
|
+
if (!isPropDescriptor(prop)) return;
|
|
1285
|
+
if (prop.merges) this.collectMergeableMetadata(path, prop, false);
|
|
1286
|
+
if (prop.isScroll) this.collectScrollMetadata(path, prop, resolved);
|
|
1287
|
+
if (prop.resolvesOnce) this.collectOnceMetadata(path, prop);
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Collect the deferred prop under its group.
|
|
1291
|
+
*
|
|
1292
|
+
* @param path Dot path of the prop.
|
|
1293
|
+
* @param prop The deferred descriptor.
|
|
1294
|
+
*/
|
|
1295
|
+
collectDeferredPropMetadata(path, prop) {
|
|
1296
|
+
const group = this.deferredProps[prop.group] ?? [];
|
|
1297
|
+
group.push(path);
|
|
1298
|
+
this.deferredProps[prop.group] = group;
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Collect the merge strategy for a mergeable prop, unless the client asked
|
|
1302
|
+
* to reset it or a partial request filters it out.
|
|
1303
|
+
*
|
|
1304
|
+
* @param path Dot path of the prop.
|
|
1305
|
+
* @param prop The mergeable descriptor.
|
|
1306
|
+
* @param excluded Whether the prop is being excluded from this response rather than resolved.
|
|
1307
|
+
*/
|
|
1308
|
+
collectMergeableMetadata(path, prop, excluded) {
|
|
1309
|
+
if (this.request.resetKeys?.includes(path)) return;
|
|
1310
|
+
if (this.isPartial && !this.isIncludedInPartialMetadata(path)) return;
|
|
1311
|
+
const layout = this.mergeLayoutFor(prop, excluded);
|
|
1312
|
+
if (prop.deep) this.deepMergeProps.push(path);
|
|
1313
|
+
else if (layout.appendsAtRoot) this.mergeProps.push(path);
|
|
1314
|
+
else if (layout.prependsAtRoot) this.prependProps.push(path);
|
|
1315
|
+
else {
|
|
1316
|
+
this.mergeProps.push(...layout.appendPaths.map((appendPath) => `${path}.${appendPath}`));
|
|
1317
|
+
this.prependProps.push(...layout.prependPaths.map((prependPath) => `${path}.${prependPath}`));
|
|
1318
|
+
}
|
|
1319
|
+
this.matchPropsOn.push(...prop.matchOnPaths.map((matchOn) => `${path}.${matchOn}`));
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* The merge layout of a descriptor for this request, read without writing
|
|
1323
|
+
* to the descriptor. A scroll descriptor derives its layout per request:
|
|
1324
|
+
* a root append while it is excluded as deferred, otherwise its wrapper key
|
|
1325
|
+
* appended or prepended according to the client's merge intent (the
|
|
1326
|
+
* read-only equivalent of `ScrollProp::configureMergeIntent`).
|
|
1327
|
+
*
|
|
1328
|
+
* @param prop The mergeable descriptor.
|
|
1329
|
+
* @param excluded Whether the prop is being excluded from this response rather than resolved.
|
|
1330
|
+
*/
|
|
1331
|
+
mergeLayoutFor(prop, excluded) {
|
|
1332
|
+
if (!prop.isScroll) return prop;
|
|
1333
|
+
if (excluded) return {
|
|
1334
|
+
appendsAtRoot: true,
|
|
1335
|
+
prependsAtRoot: false,
|
|
1336
|
+
appendPaths: [],
|
|
1337
|
+
prependPaths: []
|
|
1338
|
+
};
|
|
1339
|
+
const prepends = this.request.mergeIntent === "prepend";
|
|
1340
|
+
return {
|
|
1341
|
+
appendsAtRoot: false,
|
|
1342
|
+
prependsAtRoot: false,
|
|
1343
|
+
appendPaths: prepends ? [] : [prop.wrapper],
|
|
1344
|
+
prependPaths: prepends ? [prop.wrapper] : []
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Determine if the path should contribute metadata during a partial request.
|
|
1349
|
+
*
|
|
1350
|
+
* @param path Dot path of the prop.
|
|
1351
|
+
*/
|
|
1352
|
+
isIncludedInPartialMetadata(path) {
|
|
1353
|
+
if (this.only !== null && !matchesOnly(path, this.only)) return false;
|
|
1354
|
+
if (this.except !== null && matchesExcept(path, this.except)) return false;
|
|
1355
|
+
return true;
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Collect scroll pagination metadata, flagged for reset when the client
|
|
1359
|
+
* asked to reset the prop.
|
|
1360
|
+
*
|
|
1361
|
+
* @param path Dot path of the prop.
|
|
1362
|
+
* @param prop The scroll descriptor.
|
|
1363
|
+
* @param resolved The prop's resolved value.
|
|
1364
|
+
*/
|
|
1365
|
+
collectScrollMetadata(path, prop, resolved) {
|
|
1366
|
+
this.scrollProps[path] = {
|
|
1367
|
+
...prop.metadataFor(resolved),
|
|
1368
|
+
reset: this.request.resetKeys?.includes(path) ?? false
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Collect once-prop metadata under its custom key (or path), unless a
|
|
1373
|
+
* partial request filters it out.
|
|
1374
|
+
*
|
|
1375
|
+
* @param path Dot path of the prop.
|
|
1376
|
+
* @param prop The once descriptor.
|
|
1377
|
+
*/
|
|
1378
|
+
collectOnceMetadata(path, prop) {
|
|
1379
|
+
if (!prop.resolvesOnce) return;
|
|
1380
|
+
if (this.isPartial && !this.isIncludedInPartialMetadata(path)) return;
|
|
1381
|
+
this.onceProps[prop.onceKey ?? path] = {
|
|
1382
|
+
prop: path,
|
|
1383
|
+
expiresAt: prop.expiresAt()
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
};
|
|
1387
|
+
/**
|
|
1388
|
+
* Resolve shared and page props for one response.
|
|
1389
|
+
*
|
|
1390
|
+
* @param input Props, shared props, component, request state and rescue hook.
|
|
1391
|
+
*/
|
|
1392
|
+
async function resolveProps(input) {
|
|
1393
|
+
return new PropsResolver(input.request, input.component, input.onRescue).resolve(input.shared, input.props);
|
|
1394
|
+
}
|
|
1395
|
+
//#endregion
|
|
1396
|
+
//#region src/core/redirects.ts
|
|
1397
|
+
/**
|
|
1398
|
+
* Status code Inertia uses for "soft" redirects after PUT/PATCH/DELETE so the
|
|
1399
|
+
* follow-up GET is forced to also be a GET on every browser.
|
|
1400
|
+
*/
|
|
1401
|
+
const INERTIA_REDIRECT_STATUS = 303;
|
|
1402
|
+
/**
|
|
1403
|
+
* Status code used for the `X-Inertia-Location` external redirect
|
|
1404
|
+
* mechanism. The client intercepts the 409 and performs a window.location
|
|
1405
|
+
* navigation rather than a fetch.
|
|
1406
|
+
*/
|
|
1407
|
+
const INERTIA_LOCATION_STATUS = 409;
|
|
1408
|
+
/**
|
|
1409
|
+
* Decide which status code to use for a redirect produced by an Inertia
|
|
1410
|
+
* request. After a non-GET/HEAD form submission the response must be 303
|
|
1411
|
+
* so the client's follow-up request becomes a GET.
|
|
1412
|
+
*
|
|
1413
|
+
* @param method HTTP method of the inbound request (case-insensitive).
|
|
1414
|
+
* @param fallback Status code to use for GET/HEAD/POST. Defaults to 302.
|
|
1415
|
+
*/
|
|
1416
|
+
function inertiaRedirectStatus(method, fallback = 302) {
|
|
1417
|
+
const m = method.toUpperCase();
|
|
1418
|
+
if (m === "PUT" || m === "PATCH" || m === "DELETE") return 303;
|
|
1419
|
+
return fallback;
|
|
1420
|
+
}
|
|
1421
|
+
//#endregion
|
|
1422
|
+
//#region src/core/render.ts
|
|
1423
|
+
/**
|
|
1424
|
+
* Resolve a `SharedPropsInput` to a plain props object. Returns `{}` for
|
|
1425
|
+
* undefined/null inputs or when the resolver returns nullish.
|
|
1426
|
+
*
|
|
1427
|
+
* @param shared Static props object, a (possibly async) function, or undefined.
|
|
1428
|
+
* @param req Request passed to the resolver when `shared` is a function.
|
|
1429
|
+
*/
|
|
1430
|
+
async function evaluateShared(shared, req) {
|
|
1431
|
+
if (shared === void 0 || shared === null) return {};
|
|
1432
|
+
if (typeof shared === "function") return await shared(req) ?? {};
|
|
1433
|
+
return shared;
|
|
1434
|
+
}
|
|
1435
|
+
/**
|
|
1436
|
+
* Build the final {@link InertiaPage} object given a controller's component
|
|
1437
|
+
* + props, the request info, and configured shared props / version.
|
|
1438
|
+
*
|
|
1439
|
+
* Returned page is suitable for either:
|
|
1440
|
+
* - JSON serialization on `X-Inertia` requests, or
|
|
1441
|
+
* - embedding into the root HTML view via a `<script>` page tag.
|
|
1442
|
+
*
|
|
1443
|
+
* @param input Component, props, parsed request, shared resolver, version,
|
|
1444
|
+
* optional `clearHistory` / `encryptHistory` / `preserveFragment` flags, and
|
|
1445
|
+
* an optional rescue handler.
|
|
1446
|
+
*/
|
|
1447
|
+
async function buildPage(input) {
|
|
1448
|
+
const sharedProps = await evaluateShared(input.shared, input.req);
|
|
1449
|
+
const resolved = await resolveProps({
|
|
1450
|
+
props: input.props,
|
|
1451
|
+
shared: sharedProps,
|
|
1452
|
+
component: input.component,
|
|
1453
|
+
request: input.request,
|
|
1454
|
+
onRescue: input.onRescue
|
|
1455
|
+
});
|
|
1456
|
+
return createPage({
|
|
1457
|
+
component: input.component,
|
|
1458
|
+
props: resolved.props,
|
|
1459
|
+
url: input.request.url,
|
|
1460
|
+
version: input.version,
|
|
1461
|
+
clearHistory: input.options?.clearHistory,
|
|
1462
|
+
encryptHistory: input.options?.encryptHistory,
|
|
1463
|
+
preserveFragment: input.options?.preserveFragment,
|
|
1464
|
+
metadata: resolved.metadata,
|
|
1465
|
+
flash: input.flash
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
//#endregion
|
|
1469
|
+
//#region src/core/versioning.ts
|
|
1470
|
+
/**
|
|
1471
|
+
* Resolve a version value (string, async function, or null) against a request.
|
|
1472
|
+
*
|
|
1473
|
+
* Returns `null` when no version is configured — in which case asset version
|
|
1474
|
+
* mismatch detection is disabled.
|
|
1475
|
+
*
|
|
1476
|
+
* @param resolver Static version string, a (possibly async) function, or undefined.
|
|
1477
|
+
* @param req Request passed to the resolver when it is a function.
|
|
1478
|
+
*/
|
|
1479
|
+
async function resolveVersion(resolver, req) {
|
|
1480
|
+
if (resolver === void 0 || resolver === null) return null;
|
|
1481
|
+
if (typeof resolver === "string") return resolver;
|
|
1482
|
+
return await resolver(req) ?? null;
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* Returns true if the client's version (`X-Inertia-Version`) does not match
|
|
1486
|
+
* the server's current version. Mismatches on GET requests should trigger a
|
|
1487
|
+
* full reload via the `X-Inertia-Location` mechanism.
|
|
1488
|
+
*
|
|
1489
|
+
* @param serverVersion Currently configured server version; `null` disables checks.
|
|
1490
|
+
* @param clientVersion Value of the `X-Inertia-Version` header; `null` is treated as empty.
|
|
1491
|
+
*/
|
|
1492
|
+
function isVersionMismatch(serverVersion, clientVersion) {
|
|
1493
|
+
if (serverVersion === null) return false;
|
|
1494
|
+
return serverVersion !== (clientVersion ?? "");
|
|
1495
|
+
}
|
|
1496
|
+
//#endregion
|
|
1497
|
+
export { canonicalStringify as A, defer as C, optional as D, once as E, scroll as O, deepMerge as S, merge as T, INERTIA_HEADERS as _, INERTIA_REDIRECT_STATUS as a, unpackDotProps as b, resolveProps as c, matchesOnly as d, pathIsSameOrDescendant as f, renderErrorPage as g, renderDefaultHtml as h, INERTIA_LOCATION_STATUS as i, computeSsrCacheKey as j, PropDescriptor as k, leadsToOnly as l, encodePageScript as m, resolveVersion as n, inertiaRedirectStatus as o, createPage as p, buildPage as r, PropsResolver as s, isVersionMismatch as t, matchesExcept as u, parseInertiaRequest as v, isPropDescriptor as w, always as x, scopeErrors as y };
|
|
1498
|
+
|
|
1499
|
+
//# sourceMappingURL=core-ChQbBw-k.js.map
|