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