@orieljs/inertia 0.1.0-alpha.2

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/dist/index.js ADDED
@@ -0,0 +1,1412 @@
1
+ // headers.ts
2
+ var Header = {
3
+ INERTIA: "X-Inertia",
4
+ ERROR_BAG: "X-Inertia-Error-Bag",
5
+ LOCATION: "X-Inertia-Location",
6
+ REDIRECT: "X-Inertia-Redirect",
7
+ VERSION: "X-Inertia-Version",
8
+ PARTIAL_COMPONENT: "X-Inertia-Partial-Component",
9
+ PARTIAL_ONLY: "X-Inertia-Partial-Data",
10
+ PARTIAL_EXCEPT: "X-Inertia-Partial-Except",
11
+ RESET: "X-Inertia-Reset",
12
+ INFINITE_SCROLL_MERGE_INTENT: "X-Inertia-Infinite-Scroll-Merge-Intent",
13
+ EXCEPT_ONCE_PROPS: "X-Inertia-Except-Once-Props"
14
+ };
15
+ function parseList(value) {
16
+ const items = (value ?? "").split(",").filter((item) => item !== "");
17
+ return items.length === 0 ? void 0 : items;
18
+ }
19
+
20
+ // props/Prop.ts
21
+ var Prop = class {
22
+ constructor(value) {
23
+ this.value = value;
24
+ }
25
+ value;
26
+ mergeState = { merge: false, deep: false, append: true, appendPaths: [], prependPaths: [], matchOn: [] };
27
+ onceState = { once: false, fresh: false, key: void 0, ttlSeconds: void 0 };
28
+ deferred = false;
29
+ deferGroup;
30
+ /** Calls the callback, if the value is one. */
31
+ async resolve() {
32
+ return typeof this.value === "function" ? await this.value() : this.value;
33
+ }
34
+ /** Never sent on the first load (OptionalProp, DeferProp). */
35
+ ignoresFirstLoad() {
36
+ return false;
37
+ }
38
+ /** Sent even when a partial reload didn't ask for it (AlwaysProp). */
39
+ isAlways() {
40
+ return false;
41
+ }
42
+ /** Errors are reported and the prop is left out, instead of failing the page (defer(..., { rescue: true })). */
43
+ shouldRescue() {
44
+ return false;
45
+ }
46
+ // Once (honoured by OptionalProp, DeferProp, MergeProp and OnceProp) ------------------------------------
47
+ /** once(), once(false), once(true, { as: 'key', until: 3600 }). */
48
+ once(value = true, options = {}) {
49
+ this.onceState.once = value;
50
+ if (options.as !== void 0) this.onceState.key = options.as;
51
+ if (options.until !== void 0) this.onceState.ttlSeconds = secondsUntil(options.until);
52
+ return this;
53
+ }
54
+ /** A custom once key, so pages can share the prop under different names. */
55
+ as(key) {
56
+ this.onceState.key = key;
57
+ return this;
58
+ }
59
+ /** Send the prop again even though the client already has it. */
60
+ fresh(value = true) {
61
+ this.onceState.fresh = value;
62
+ return this;
63
+ }
64
+ /** Expire after a number of seconds, or at a Date. */
65
+ until(delay) {
66
+ this.onceState.ttlSeconds = secondsUntil(delay);
67
+ return this;
68
+ }
69
+ // Merging -------------------------------------------------------------------------------------------------
70
+ isMergeable() {
71
+ return false;
72
+ }
73
+ shouldMerge() {
74
+ return this.isMergeable() && this.mergeState.merge;
75
+ }
76
+ shouldDeepMerge() {
77
+ return this.mergeState.deep;
78
+ }
79
+ matchesOn() {
80
+ return [...this.mergeState.matchOn];
81
+ }
82
+ appendsAtRoot() {
83
+ return this.mergeState.append && this.mergesAtRoot();
84
+ }
85
+ prependsAtRoot() {
86
+ return !this.mergeState.append && this.mergesAtRoot();
87
+ }
88
+ appendsAtPaths() {
89
+ return [...this.mergeState.appendPaths];
90
+ }
91
+ prependsAtPaths() {
92
+ return [...this.mergeState.prependPaths];
93
+ }
94
+ mergesAtRoot() {
95
+ return this.mergeState.appendPaths.length === 0 && this.mergeState.prependPaths.length === 0;
96
+ }
97
+ // Deferring -----------------------------------------------------------------------------------------------
98
+ isDeferrable() {
99
+ return false;
100
+ }
101
+ shouldDefer() {
102
+ return this.isDeferrable() && this.deferred;
103
+ }
104
+ group() {
105
+ return this.deferGroup ?? "default";
106
+ }
107
+ // Once ----------------------------------------------------------------------------------------------------
108
+ isOnceable() {
109
+ return false;
110
+ }
111
+ shouldResolveOnce() {
112
+ return this.isOnceable() && this.onceState.once;
113
+ }
114
+ shouldBeRefreshed() {
115
+ return this.onceState.fresh;
116
+ }
117
+ getKey() {
118
+ return this.onceState.key;
119
+ }
120
+ /** Epoch milliseconds, or null when the prop never expires. */
121
+ expiresAt() {
122
+ return this.onceState.ttlSeconds === void 0 ? null : (Math.floor(Date.now() / 1e3) + this.onceState.ttlSeconds) * 1e3;
123
+ }
124
+ };
125
+ var MergeableProp = class extends Prop {
126
+ isMergeable() {
127
+ return true;
128
+ }
129
+ merge() {
130
+ this.mergeState.merge = true;
131
+ return this;
132
+ }
133
+ deepMerge() {
134
+ this.mergeState.deep = true;
135
+ return this.merge();
136
+ }
137
+ /** Match array items on these keys instead of appending duplicates: matchOn('id'). */
138
+ matchOn(keys) {
139
+ this.mergeState.matchOn = [keys].flat();
140
+ return this;
141
+ }
142
+ /** append() at the root, append('data') at a path, append('data', 'id') with a match key, or a map of both. */
143
+ append(path = true, matchOn) {
144
+ return this.#place("append", path, matchOn);
145
+ }
146
+ prepend(path = true, matchOn) {
147
+ return this.#place("prepend", path, matchOn);
148
+ }
149
+ #place(side, path, matchOn) {
150
+ if (typeof path === "boolean") {
151
+ this.mergeState.append = side === "append" ? path : !path;
152
+ return this;
153
+ }
154
+ if (Array.isArray(path)) {
155
+ for (const entry of path) this.#place(side, entry);
156
+ return this;
157
+ }
158
+ if (typeof path === "object") {
159
+ for (const [entry, key] of Object.entries(path)) this.#place(side, entry, key);
160
+ return this;
161
+ }
162
+ ;
163
+ (side === "append" ? this.mergeState.appendPaths : this.mergeState.prependPaths).push(path);
164
+ if (matchOn !== void 0 && matchOn !== "") this.mergeState.matchOn = [...this.mergeState.matchOn, `${path}.${matchOn}`];
165
+ return this;
166
+ }
167
+ };
168
+ function secondsUntil(delay) {
169
+ return delay instanceof Date ? Math.max(0, Math.round((delay.getTime() - Date.now()) / 1e3)) : delay;
170
+ }
171
+
172
+ // props/props.ts
173
+ var AlwaysProp = class extends Prop {
174
+ kind = "always";
175
+ isAlways() {
176
+ return true;
177
+ }
178
+ };
179
+ var OptionalProp = class extends Prop {
180
+ kind = "optional";
181
+ constructor(callback) {
182
+ super(callback);
183
+ }
184
+ ignoresFirstLoad() {
185
+ return true;
186
+ }
187
+ isOnceable() {
188
+ return true;
189
+ }
190
+ };
191
+ var DeferProp = class extends MergeableProp {
192
+ kind = "defer";
193
+ #rescue;
194
+ constructor(callback, group, options = {}) {
195
+ super(callback);
196
+ this.deferred = true;
197
+ this.deferGroup = group;
198
+ this.#rescue = options.rescue ?? false;
199
+ }
200
+ ignoresFirstLoad() {
201
+ return true;
202
+ }
203
+ isDeferrable() {
204
+ return true;
205
+ }
206
+ isOnceable() {
207
+ return true;
208
+ }
209
+ shouldRescue() {
210
+ return this.#rescue;
211
+ }
212
+ };
213
+ var MergeProp = class extends MergeableProp {
214
+ kind = "merge";
215
+ constructor(value) {
216
+ super(value);
217
+ this.mergeState.merge = true;
218
+ }
219
+ isOnceable() {
220
+ return true;
221
+ }
222
+ };
223
+ var OnceProp = class extends Prop {
224
+ kind = "once";
225
+ constructor(callback) {
226
+ super(callback);
227
+ this.onceState.once = true;
228
+ }
229
+ isOnceable() {
230
+ return true;
231
+ }
232
+ };
233
+ var isCursorPaginator = (value) => typeof value === "object" && value !== null && typeof value.getCursorName === "function";
234
+ var isPagedPaginator = (value) => typeof value === "object" && value !== null && typeof value.getPageName === "function" && typeof value.currentPage === "function" && typeof value.hasMorePages === "function";
235
+ function scrollMetadataFrom(value) {
236
+ if (isCursorPaginator(value)) {
237
+ const current = value.cursor();
238
+ return {
239
+ pageName: value.getCursorName(),
240
+ previousPage: value.previousCursor()?.encode() ?? null,
241
+ nextPage: value.nextCursor()?.encode() ?? null,
242
+ currentPage: current === void 0 ? 1 : current.encode()
243
+ };
244
+ }
245
+ if (isPagedPaginator(value)) {
246
+ const current = value.currentPage();
247
+ return {
248
+ pageName: value.getPageName(),
249
+ previousPage: current > 1 ? current - 1 : null,
250
+ nextPage: value.hasMorePages() ? current + 1 : null,
251
+ currentPage: current
252
+ };
253
+ }
254
+ throw new TypeError("The given value is not a paginator. Pass a metadata callback to Inertia.scroll().");
255
+ }
256
+ var ScrollProp = class extends MergeableProp {
257
+ constructor(value, wrapper = "data", metadataResolver) {
258
+ super(value);
259
+ this.wrapper = wrapper;
260
+ this.metadataResolver = metadataResolver;
261
+ this.mergeState.merge = true;
262
+ }
263
+ wrapper;
264
+ metadataResolver;
265
+ kind = "scroll";
266
+ #resolved;
267
+ isDeferrable() {
268
+ return true;
269
+ }
270
+ /** Load the prop after the first render, like a deferred prop. */
271
+ defer(group) {
272
+ this.deferred = true;
273
+ this.deferGroup = group;
274
+ return this;
275
+ }
276
+ /** 'prepend' from the X-Inertia-Infinite-Scroll-Merge-Intent header prepends the wrapper; anything else appends. */
277
+ configureMergeIntent(intent) {
278
+ return intent === "prepend" ? this.prepend(this.wrapper) : this.append(this.wrapper);
279
+ }
280
+ /** Resolves once and caches, because both the value and its metadata need it. */
281
+ async resolve() {
282
+ this.#resolved ??= { value: await super.resolve() };
283
+ return this.#resolved.value;
284
+ }
285
+ async metadata() {
286
+ const value = await this.resolve();
287
+ return this.metadataResolver === void 0 ? scrollMetadataFrom(value) : this.metadataResolver(value);
288
+ }
289
+ };
290
+ var isProp = (value) => value instanceof Prop;
291
+
292
+ // PropsResolver.ts
293
+ import { Arr } from "@orieljs/support";
294
+ var isContainer = (value) => {
295
+ if (Array.isArray(value)) return true;
296
+ if (typeof value !== "object" || value === null) return false;
297
+ const prototype = Object.getPrototypeOf(value);
298
+ return prototype === Object.prototype || prototype === null;
299
+ };
300
+ var entriesOf = (value) => Array.isArray(value) ? value.map((item, index) => [String(index), item]) : Object.entries(value);
301
+ var PropsResolver = class {
302
+ constructor(request, component, options = {}) {
303
+ this.options = options;
304
+ this.#isPartial = request.header(Header.PARTIAL_COMPONENT) === component;
305
+ const inertia2 = request.header(Header.INERTIA);
306
+ this.#isInertia = inertia2 !== void 0 && inertia2 !== "" && inertia2 !== "false";
307
+ this.#only = parseList(request.header(Header.PARTIAL_ONLY));
308
+ this.#except = parseList(request.header(Header.PARTIAL_EXCEPT));
309
+ this.#reset = parseList(request.header(Header.RESET)) ?? [];
310
+ this.#loadedOnce = parseList(request.header(Header.EXCEPT_ONCE_PROPS)) ?? [];
311
+ this.#mergeIntent = request.header(Header.INFINITE_SCROLL_MERGE_INTENT);
312
+ }
313
+ options;
314
+ #isPartial;
315
+ #isInertia;
316
+ #only;
317
+ #except;
318
+ #reset;
319
+ #loadedOnce;
320
+ #mergeIntent;
321
+ #deferred = {};
322
+ #rescued = [];
323
+ #merge = [];
324
+ #prepend = [];
325
+ #deepMerge = [];
326
+ #matchOn = [];
327
+ #scroll = {};
328
+ #once = {};
329
+ #observed = /* @__PURE__ */ new WeakSet();
330
+ #sharedKeys = [];
331
+ async resolve(shared, props) {
332
+ if (this.options.exposeSharedPropKeys ?? true) {
333
+ this.#sharedKeys = [...new Set(Object.keys(shared).map((key) => key.split(".")[0]))];
334
+ }
335
+ this.#observePromises(shared);
336
+ this.#observePromises(props);
337
+ const merged = await this.#unpackDotProps({ ...shared, ...props });
338
+ const resolved = await this.#resolveProps(merged, "", false);
339
+ return { props: resolved, metadata: this.#metadata() };
340
+ }
341
+ #metadata() {
342
+ const metadata = {};
343
+ if (this.#sharedKeys.length > 0) metadata.sharedProps = this.#sharedKeys;
344
+ if (this.#merge.length > 0) metadata.mergeProps = this.#merge;
345
+ if (this.#prepend.length > 0) metadata.prependProps = this.#prepend;
346
+ if (this.#deepMerge.length > 0) metadata.deepMergeProps = this.#deepMerge;
347
+ if (this.#matchOn.length > 0) metadata.matchPropsOn = this.#matchOn;
348
+ if (Object.keys(this.#deferred).length > 0) metadata.deferredProps = this.#deferred;
349
+ if (this.#rescued.length > 0) metadata.rescuedProps = this.#rescued;
350
+ if (Object.keys(this.#scroll).length > 0) metadata.scrollProps = this.#scroll;
351
+ if (Object.keys(this.#once).length > 0) metadata.onceProps = this.#once;
352
+ return metadata;
353
+ }
354
+ /**
355
+ * Marks every raw Promise in a container tree as handled. A promise the walk never awaits (filtered out by a
356
+ * partial reload, or after a prop that threw) would otherwise reject unobserved and could end the process.
357
+ * Awaiting the original promise still throws its error where it is used.
358
+ */
359
+ #observePromises(value) {
360
+ if (value instanceof Promise) {
361
+ value.catch(() => void 0);
362
+ return;
363
+ }
364
+ if (!isContainer(value) || this.#observed.has(value)) return;
365
+ this.#observed.add(value);
366
+ for (const [, child] of entriesOf(value)) this.#observePromises(child);
367
+ }
368
+ async #resolveProps(props, prefix, parentWasResolved) {
369
+ this.#observePromises(props);
370
+ const result = Array.isArray(props) ? [] : {};
371
+ for (const [key, original] of entriesOf(props)) {
372
+ const path = prefix === "" ? key : `${prefix}.${key}`;
373
+ let prop = original;
374
+ if (!this.#shouldIncludeInPartialResponse(prop, path, parentWasResolved)) continue;
375
+ if (!this.#isPartial && this.#excludeFromInitialResponse(prop, path)) continue;
376
+ let value = await this.#resolveValue(prop, path);
377
+ if (this.#rescued.includes(path)) continue;
378
+ if (value !== prop && isProp(value)) {
379
+ prop = value;
380
+ if (!this.#isPartial && this.#excludeFromInitialResponse(prop, path)) continue;
381
+ value = await this.#resolveValue(prop, path);
382
+ if (this.#rescued.includes(path)) continue;
383
+ }
384
+ await this.#collectMetadata(prop, path);
385
+ const child = isContainer(value) ? await this.#resolveProps(value, path, parentWasResolved || !isContainer(prop)) : value;
386
+ if (Array.isArray(result)) result.push(child);
387
+ else result[key] = child;
388
+ }
389
+ return result;
390
+ }
391
+ #shouldIncludeInPartialResponse(prop, path, parentWasResolved) {
392
+ if (!this.#isPartial || parentWasResolved || isProp(prop) && prop.isAlways()) return true;
393
+ if (this.#only !== void 0 && !this.#matchesOnly(path) && !this.#leadsToOnly(path)) return false;
394
+ if (this.#except !== void 0 && this.#matchesExcept(path)) return false;
395
+ return true;
396
+ }
397
+ #excludeFromInitialResponse(prop, path) {
398
+ if (!isProp(prop)) return false;
399
+ if (prop.ignoresFirstLoad()) {
400
+ if (prop.shouldDefer() && !this.#wasAlreadyLoadedByClient(prop, path)) this.#collectDeferred(path, prop);
401
+ if (prop.shouldMerge()) this.#collectMerge(path, prop);
402
+ if (prop.shouldResolveOnce()) this.#collectOnce(path, prop);
403
+ return true;
404
+ }
405
+ if (prop.shouldDefer()) {
406
+ this.#collectDeferred(path, prop);
407
+ if (prop.shouldMerge()) this.#collectMerge(path, prop);
408
+ return true;
409
+ }
410
+ if (this.#isInertia && this.#wasAlreadyLoadedByClient(prop, path)) {
411
+ this.#collectOnce(path, prop);
412
+ return true;
413
+ }
414
+ return false;
415
+ }
416
+ #wasAlreadyLoadedByClient(prop, path) {
417
+ return prop.shouldResolveOnce() && !prop.shouldBeRefreshed() && this.#loadedOnce.includes(prop.getKey() ?? path);
418
+ }
419
+ /** Callbacks and prop types run; objects with toJSON() (models, collections, paginators, dates) serialize. */
420
+ async #resolveValue(value, path) {
421
+ if (value instanceof ScrollProp) value.configureMergeIntent(this.#mergeIntent);
422
+ const rescue = isProp(value) && value.shouldRescue();
423
+ try {
424
+ let resolved = value;
425
+ if (isProp(resolved)) resolved = await resolved.resolve();
426
+ else if (typeof resolved === "function") resolved = await resolved();
427
+ else if (resolved instanceof Promise) resolved = await resolved;
428
+ if (!isProp(resolved) && typeof resolved === "object" && resolved !== null && !Array.isArray(resolved)) {
429
+ const serializable = resolved;
430
+ if (typeof serializable.toJSON === "function") resolved = serializable.toJSON();
431
+ }
432
+ return resolved;
433
+ } catch (error) {
434
+ if (!rescue) throw error;
435
+ await this.options.report?.(error);
436
+ this.#rescued.push(path);
437
+ return null;
438
+ }
439
+ }
440
+ async #collectMetadata(prop, path) {
441
+ if (!isProp(prop)) return;
442
+ if (prop.shouldMerge()) this.#collectMerge(path, prop);
443
+ if (prop instanceof ScrollProp) this.#scroll[path] = { ...await prop.metadata(), reset: this.#reset.includes(path) };
444
+ if (prop.shouldResolveOnce()) this.#collectOnce(path, prop);
445
+ }
446
+ #collectDeferred(path, prop) {
447
+ ;
448
+ (this.#deferred[prop.group()] ??= []).push(path);
449
+ }
450
+ #collectMerge(path, prop) {
451
+ if (this.#reset.includes(path)) return;
452
+ if (this.#isPartial && !this.#isIncludedInPartialMetadata(path)) return;
453
+ if (prop.shouldDeepMerge()) this.#deepMerge.push(path);
454
+ else if (prop.appendsAtRoot()) this.#merge.push(path);
455
+ else if (prop.prependsAtRoot()) this.#prepend.push(path);
456
+ else {
457
+ for (const append of prop.appendsAtPaths()) this.#merge.push(`${path}.${append}`);
458
+ for (const prepend of prop.prependsAtPaths()) this.#prepend.push(`${path}.${prepend}`);
459
+ }
460
+ for (const strategy of prop.matchesOn()) this.#matchOn.push(`${path}.${strategy}`);
461
+ }
462
+ #collectOnce(path, prop) {
463
+ if (!prop.shouldResolveOnce()) return;
464
+ if (this.#isPartial && !this.#isIncludedInPartialMetadata(path)) return;
465
+ this.#once[prop.getKey() ?? path] = { prop: path, expiresAt: prop.expiresAt() };
466
+ }
467
+ #isIncludedInPartialMetadata(path) {
468
+ if (this.#only !== void 0 && !this.#matchesOnly(path)) return false;
469
+ if (this.#except !== void 0 && this.#matchesExcept(path)) return false;
470
+ return true;
471
+ }
472
+ #matchesOnly(path) {
473
+ return this.#only.some((only) => path === only || path.startsWith(`${only}.`));
474
+ }
475
+ #leadsToOnly(path) {
476
+ return this.#only.some((only) => only.startsWith(`${path}.`));
477
+ }
478
+ #matchesExcept(path) {
479
+ return this.#except.some((except) => path === except || path.startsWith(`${except}.`));
480
+ }
481
+ /** 'auth.user' => value becomes { auth: { user: value } }; callbacks along the path are resolved first. */
482
+ async #unpackDotProps(props) {
483
+ const result = { ...props };
484
+ for (const [key, original] of Object.entries(props)) {
485
+ if (!key.includes(".")) continue;
486
+ let value = original;
487
+ if (typeof value === "function") value = await value();
488
+ await this.#ensurePathIsTraversable(result, key);
489
+ delete result[key];
490
+ Arr.set(result, key, value);
491
+ }
492
+ return result;
493
+ }
494
+ async #ensurePathIsTraversable(props, dotKey) {
495
+ let current = props;
496
+ for (const segment of dotKey.split(".").slice(0, -1)) {
497
+ if (current[segment] === void 0 || current[segment] === null) return;
498
+ if (typeof current[segment] === "function") current[segment] = await current[segment]();
499
+ const value = current[segment];
500
+ if (typeof value === "object" && value !== null && !isContainer(value) && typeof value.toJSON === "function") {
501
+ current[segment] = value.toJSON();
502
+ }
503
+ const walked = current[segment];
504
+ if (typeof walked !== "object" || walked === null) return;
505
+ current[segment] = Array.isArray(walked) ? [...walked] : Object.assign(Object.create(Object.getPrototypeOf(walked)), walked);
506
+ current = current[segment];
507
+ }
508
+ }
509
+ };
510
+
511
+ // context.ts
512
+ import { HttpRequest, Session } from "@orieljs/contracts";
513
+ import { currentContainer } from "@orieljs/container";
514
+ function requestScope() {
515
+ const container = currentContainer();
516
+ return container !== void 0 && container.bound(HttpRequest) ? container : void 0;
517
+ }
518
+ function currentRequest() {
519
+ return requestScope()?.make(HttpRequest);
520
+ }
521
+ function optionalSession() {
522
+ const scope = requestScope();
523
+ return scope?.bound(Session) ? scope.make(Session) : void 0;
524
+ }
525
+ function isInertiaRequest(request) {
526
+ const value = request.header("X-Inertia");
527
+ return value !== void 0 && value !== "" && value !== "false";
528
+ }
529
+ function isPrefetch(request) {
530
+ return ["Purpose", "Sec-Purpose", "X-Moz"].some((name) => request.header(name)?.toLowerCase() === "prefetch");
531
+ }
532
+ function rebuild(response, changes) {
533
+ const headers = new Headers(response.headers);
534
+ for (const [name, value] of Object.entries(changes.headers ?? {})) headers.set(name, value);
535
+ const status = changes.status ?? response.status;
536
+ return new Response(response.body, {
537
+ status,
538
+ statusText: status === response.status ? response.statusText : "",
539
+ headers
540
+ });
541
+ }
542
+ function isRedirect(response) {
543
+ return [201, 301, 302, 303, 307, 308].includes(response.status) && response.headers.has("Location");
544
+ }
545
+
546
+ // errors.ts
547
+ var ComponentNotFoundError = class extends Error {
548
+ constructor(component) {
549
+ super(`Inertia page component [${component}] not found.`);
550
+ this.component = component;
551
+ this.name = "ComponentNotFoundError";
552
+ }
553
+ component;
554
+ kind = "Server";
555
+ status = 500;
556
+ };
557
+ var InertiaError = class extends Error {
558
+ kind = "Server";
559
+ status = 500;
560
+ constructor(message) {
561
+ super(message);
562
+ this.name = "InertiaError";
563
+ }
564
+ };
565
+ var SsrError = class extends Error {
566
+ kind = "Server";
567
+ status = 500;
568
+ constructor(message, options) {
569
+ super(message, options);
570
+ this.name = "SsrError";
571
+ }
572
+ };
573
+
574
+ // Response.ts
575
+ import { existsSync } from "fs";
576
+ import { join } from "path";
577
+ import {
578
+ ConfigRepository,
579
+ ExceptionHandler,
580
+ ViewFactory
581
+ } from "@orieljs/contracts";
582
+ var SessionKey = {
583
+ CLEAR_HISTORY: "inertia.clear_history",
584
+ PRESERVE_FRAGMENT: "inertia.preserve_fragment",
585
+ FLASH_DATA: "inertia.flash_data"
586
+ };
587
+ function defaultUrl(request) {
588
+ const url = new URL(request.raw.url);
589
+ return `${url.pathname}${url.search}`;
590
+ }
591
+ var Response2 = class {
592
+ constructor(component, props, settings) {
593
+ this.component = component;
594
+ this.settings = settings;
595
+ this.#props = { ...props };
596
+ this.#rootView = settings.rootView;
597
+ }
598
+ component;
599
+ settings;
600
+ #props;
601
+ #viewData = {};
602
+ #rootView;
603
+ with(key, value) {
604
+ if (typeof key === "string") this.#props[key] = value;
605
+ else Object.assign(this.#props, key);
606
+ return this;
607
+ }
608
+ withViewData(key, value) {
609
+ if (typeof key === "string") this.#viewData[key] = value;
610
+ else Object.assign(this.#viewData, key);
611
+ return this;
612
+ }
613
+ rootView(view) {
614
+ this.#rootView = view;
615
+ return this;
616
+ }
617
+ flash(key, value) {
618
+ this.settings.flash(key, value);
619
+ return this;
620
+ }
621
+ /** The page object for this request. */
622
+ async toPage(request) {
623
+ const scope = requestScope();
624
+ this.#ensurePageExists(scope);
625
+ const resolver = new PropsResolver(request, this.component, {
626
+ exposeSharedPropKeys: this.#config(scope, "inertia.expose_shared_prop_keys", true),
627
+ report: async (error) => {
628
+ if (scope?.bound(ExceptionHandler)) await scope.make(ExceptionHandler).report(error);
629
+ }
630
+ });
631
+ const { props, metadata } = await resolver.resolve(this.settings.shared, this.#props);
632
+ const session = optionalSession();
633
+ const clearHistory = session?.pull(SessionKey.CLEAR_HISTORY, false) === true;
634
+ const preserveFragment = session?.pull(SessionKey.PRESERVE_FRAGMENT, false) === true;
635
+ const flash = session?.pull(SessionKey.FLASH_DATA, void 0);
636
+ const page = {
637
+ component: this.component,
638
+ props,
639
+ url: (this.settings.urlResolver ?? defaultUrl)(request),
640
+ version: await this.settings.version(),
641
+ ...metadata
642
+ };
643
+ if (clearHistory) page.clearHistory = true;
644
+ if (this.settings.encryptHistory) page.encryptHistory = true;
645
+ if (flash !== void 0 && flash !== null && Object.keys(flash).length > 0) page.flash = flash;
646
+ if (preserveFragment) page.preserveFragment = true;
647
+ return page;
648
+ }
649
+ async toResponse(request) {
650
+ const http = request;
651
+ if (typeof http?.header !== "function") throw new InertiaError("An Inertia response needs the HttpRequest.");
652
+ const page = await this.toPage(http);
653
+ if (isInertiaRequest(http)) {
654
+ return new globalThis.Response(JSON.stringify(page), {
655
+ status: 200,
656
+ headers: { "content-type": "application/json", "X-Inertia": "true" }
657
+ });
658
+ }
659
+ const scope = requestScope();
660
+ if (scope === void 0 || !scope.bound(ViewFactory)) {
661
+ throw new InertiaError(`Cannot render the Inertia root view [${this.#rootView}]: no ViewFactory is bound.`);
662
+ }
663
+ const html = await scope.make(ViewFactory).make(this.#rootView, { ...this.#viewData, page }).render();
664
+ return new globalThis.Response(html, { status: 200, headers: { "content-type": "text/html; charset=utf-8" } });
665
+ }
666
+ /** inertia.pages.ensure_pages_exist: the component must exist under one of the page paths. */
667
+ #ensurePageExists(scope) {
668
+ if (!this.#config(scope, "inertia.pages.ensure_pages_exist", false)) return;
669
+ const paths = this.#config(scope, "inertia.pages.paths", []);
670
+ const extensions = this.#config(scope, "inertia.pages.extensions", []);
671
+ if (!pageExists(this.component, paths, extensions)) throw new ComponentNotFoundError(this.component);
672
+ }
673
+ #config(scope, key, fallback) {
674
+ return scope?.bound(ConfigRepository) ? scope.make(ConfigRepository).get(key, fallback) : fallback;
675
+ }
676
+ };
677
+ function pageExists(component, paths, extensions) {
678
+ return paths.some((path) => extensions.some((extension) => existsSync(join(path, `${component}.${extension}`))));
679
+ }
680
+
681
+ // ResponseFactory.ts
682
+ import {
683
+ ConfigRepository as ConfigRepository2,
684
+ Redirector
685
+ } from "@orieljs/contracts";
686
+ import { Arr as Arr2 } from "@orieljs/support";
687
+ var ResponseFactory = class _ResponseFactory {
688
+ constructor(app, parent) {
689
+ this.app = app;
690
+ this.parent = parent;
691
+ }
692
+ app;
693
+ parent;
694
+ #shared = {};
695
+ #rootView;
696
+ #version;
697
+ #encryptHistory;
698
+ #urlResolver;
699
+ /** A per-request child of this (root) factory. */
700
+ forRequest() {
701
+ return new _ResponseFactory(this.app, this);
702
+ }
703
+ setRootView(view) {
704
+ this.#rootView = view;
705
+ }
706
+ getRootView() {
707
+ return this.#rootView ?? this.parent?.getRootView() ?? "app";
708
+ }
709
+ share(key, value) {
710
+ if (typeof key === "string") Arr2.set(this.#shared, key, value);
711
+ else Object.assign(this.#shared, key);
712
+ }
713
+ shareOnce(key, callback) {
714
+ const prop = new OnceProp(callback);
715
+ this.share(key, prop);
716
+ return prop;
717
+ }
718
+ getShared(key, defaultValue) {
719
+ const shared = { ...this.parent?.getShared(), ...this.#shared };
720
+ return key === void 0 ? shared : Arr2.get(shared, key, defaultValue);
721
+ }
722
+ flushShared() {
723
+ this.#shared = {};
724
+ }
725
+ version(version) {
726
+ this.#version = version;
727
+ }
728
+ /** The asset version as a string ('' when there is none), like Laravel's `(string) $version`. */
729
+ async getVersion() {
730
+ const version = this.#versionResolver();
731
+ const resolved = typeof version === "function" ? await version() : version;
732
+ return resolved === null || resolved === void 0 ? "" : String(resolved);
733
+ }
734
+ #versionResolver() {
735
+ return this.#version ?? (this.parent === void 0 ? void 0 : this.parent.#versionResolver());
736
+ }
737
+ #encrypts() {
738
+ return this.#encryptHistory ?? (this.parent === void 0 ? void 0 : this.parent.#encrypts());
739
+ }
740
+ #resolvesUrl() {
741
+ return this.#urlResolver ?? (this.parent === void 0 ? void 0 : this.parent.#resolvesUrl());
742
+ }
743
+ resolveUrlUsing(resolver) {
744
+ this.#urlResolver = resolver;
745
+ }
746
+ encryptHistory(encrypt = true) {
747
+ this.#encryptHistory = encrypt;
748
+ }
749
+ clearHistory() {
750
+ optionalSession()?.put(SessionKey.CLEAR_HISTORY, true);
751
+ }
752
+ preserveFragment() {
753
+ optionalSession()?.put(SessionKey.PRESERVE_FRAGMENT, true);
754
+ }
755
+ optional(callback) {
756
+ return new OptionalProp(callback);
757
+ }
758
+ defer(callback, group = "default", options = {}) {
759
+ return new DeferProp(callback, group, options);
760
+ }
761
+ always(value) {
762
+ return new AlwaysProp(value);
763
+ }
764
+ merge(value) {
765
+ return new MergeProp(value);
766
+ }
767
+ deepMerge(value) {
768
+ return new MergeProp(value).deepMerge();
769
+ }
770
+ once(callback) {
771
+ return new OnceProp(callback);
772
+ }
773
+ scroll(value, wrapper = "data", metadata) {
774
+ return new ScrollProp(value, wrapper, metadata);
775
+ }
776
+ /** Flashes page.flash data for the next Inertia response (stored in the session immediately). */
777
+ flash(key, value) {
778
+ const session = optionalSession();
779
+ if (session === void 0) return this;
780
+ const flash = typeof key === "string" ? { [key]: value } : key;
781
+ session.flash(SessionKey.FLASH_DATA, { ...session.get(SessionKey.FLASH_DATA, {}), ...flash });
782
+ return this;
783
+ }
784
+ back(fallback, status = 302) {
785
+ const scope = requestScope() ?? this.app;
786
+ return scope.make(Redirector).back(fallback, status);
787
+ }
788
+ /** 409 + X-Inertia-Location for Inertia requests (the client does a full visit), a plain 302 otherwise. */
789
+ location(url) {
790
+ const request = currentRequest();
791
+ if (request !== void 0 && isInertiaRequest(request)) {
792
+ return new globalThis.Response(null, { status: 409, headers: { [Header.LOCATION]: url } });
793
+ }
794
+ return new globalThis.Response(null, { status: 302, headers: { Location: url } });
795
+ }
796
+ render(component, props = {}) {
797
+ const config = this.app.bound(ConfigRepository2) ? this.app.make(ConfigRepository2) : void 0;
798
+ return new Response2(component, props, {
799
+ shared: this.getShared(),
800
+ rootView: this.getRootView(),
801
+ version: () => this.getVersion(),
802
+ encryptHistory: this.#encrypts() ?? config?.get("inertia.history.encrypt", false) ?? false,
803
+ urlResolver: this.#resolvesUrl(),
804
+ flash: (key, value) => void this.flash(key, value)
805
+ });
806
+ }
807
+ };
808
+
809
+ // helpers.ts
810
+ import { Application, HttpRequest as HttpRequest2, InertiaFactory } from "@orieljs/contracts";
811
+ import { ContainerNotAvailableError, currentContainer as currentContainer2 } from "@orieljs/container";
812
+ function inertiaFactory() {
813
+ const container = currentContainer2();
814
+ if (container === void 0) throw new ContainerNotAvailableError("Inertia");
815
+ if (container.bound(HttpRequest2)) return container.make(InertiaFactory);
816
+ return container.make(Application).make(ResponseFactory);
817
+ }
818
+ function inertia(component, props = {}) {
819
+ const factory = inertiaFactory();
820
+ return component === void 0 ? factory : factory.render(component, props);
821
+ }
822
+ var Inertia = new Proxy({}, {
823
+ get(_target, property) {
824
+ if (typeof property === "symbol" || property === "then") return void 0;
825
+ const factory = inertiaFactory();
826
+ const value = Reflect.get(factory, property, factory);
827
+ return typeof value === "function" ? value.bind(factory) : value;
828
+ }
829
+ });
830
+
831
+ // html.ts
832
+ import { InertiaSsrGateway } from "@orieljs/contracts";
833
+ import { currentContainer as currentContainer3 } from "@orieljs/container";
834
+ var InertiaHtml = class {
835
+ constructor(html) {
836
+ this.html = html;
837
+ }
838
+ html;
839
+ toHtml() {
840
+ return this.html;
841
+ }
842
+ toString() {
843
+ return this.html;
844
+ }
845
+ };
846
+ function pageJson(page) {
847
+ return JSON.stringify(page).replace(/\//g, "\\/").replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
848
+ }
849
+ function pageMarkup(page, id = "app") {
850
+ const safeId = id.replace(/[^A-Za-z0-9_-]/g, "");
851
+ return `<script data-page="${safeId}" type="application/json">${pageJson(page)}</script><div id="${safeId}"></div>`;
852
+ }
853
+ var dispatched = /* @__PURE__ */ new WeakMap();
854
+ function ssr(page) {
855
+ let pending = dispatched.get(page);
856
+ if (pending === void 0) {
857
+ const container = currentContainer3();
858
+ pending = container?.bound(InertiaSsrGateway) ? container.make(InertiaSsrGateway).dispatch(page) : Promise.resolve(void 0);
859
+ dispatched.set(page, pending);
860
+ }
861
+ return pending;
862
+ }
863
+ function handled(promise) {
864
+ promise.catch(() => void 0);
865
+ return promise;
866
+ }
867
+ function inertiaHead(page) {
868
+ return handled(ssr(page).then((rendered) => new InertiaHtml(rendered?.head ?? "")));
869
+ }
870
+ function inertiaBody(page, id = "app") {
871
+ return handled(ssr(page).then((rendered) => new InertiaHtml(rendered?.body ?? pageMarkup(page, id))));
872
+ }
873
+
874
+ // Middleware.ts
875
+ import { createHash } from "crypto";
876
+ import { existsSync as existsSync2, readFileSync } from "fs";
877
+ import {
878
+ Application as Application2,
879
+ ConfigRepository as ConfigRepository3,
880
+ Container,
881
+ InertiaFactory as InertiaFactory2,
882
+ Redirector as Redirector2
883
+ } from "@orieljs/contracts";
884
+ import { inject } from "@orieljs/container";
885
+
886
+ // ssr/requests.ts
887
+ import { Str } from "@orieljs/support";
888
+ var excluded = /* @__PURE__ */ new WeakSet();
889
+ function withoutSsr(request, paths) {
890
+ const matches = paths.some((pattern) => {
891
+ const trimmed = pattern === "/" ? pattern : pattern.replace(/^\/+|\/+$/g, "");
892
+ return Str.is(trimmed, request.fullUrl()) || request.is(trimmed);
893
+ });
894
+ if (matches) excluded.add(request);
895
+ }
896
+ function isExcludedFromSsr(request) {
897
+ return excluded.has(request);
898
+ }
899
+
900
+ // Middleware.ts
901
+ var InertiaMiddleware = class {
902
+ /** The root view rendered on the first visit: resources/views/app.tsx. */
903
+ rootViewName = "app";
904
+ /** Send every message per field instead of the first one. */
905
+ withAllErrors = false;
906
+ /** Paths (wildcards allowed) rendered without SSR. */
907
+ withoutSsr = [];
908
+ container = inject(Container);
909
+ /** The asset version: a hash of app.asset_url, else of public/build/manifest.json, else none. */
910
+ async version(_request) {
911
+ const config = this.container.bound(ConfigRepository3) ? this.container.make(ConfigRepository3) : void 0;
912
+ const assetUrl = config?.get("app.asset_url", null);
913
+ if (typeof assetUrl === "string" && assetUrl !== "") return md5(assetUrl);
914
+ const app = this.container.make(Application2);
915
+ const manifest = app.publicPath("build/manifest.json");
916
+ return existsSync2(manifest) ? md5(readFileSync(manifest)) : null;
917
+ }
918
+ /** Props shared with every page. Override and spread `await super.share(request)` into your own. */
919
+ async share(request) {
920
+ return { errors: new AlwaysProp(this.resolveValidationErrors(request)) };
921
+ }
922
+ /** Shared once props: sent on the first visit, then kept by the client. */
923
+ async shareOnce(_request) {
924
+ return {};
925
+ }
926
+ rootView(_request) {
927
+ return this.rootViewName;
928
+ }
929
+ /** How page.url is built; undefined keeps the default (path + query). */
930
+ urlResolver() {
931
+ return void 0;
932
+ }
933
+ async handle(request, next) {
934
+ const inertia2 = this.container.make(InertiaFactory2);
935
+ inertia2.version(() => this.version(request));
936
+ inertia2.share(await this.share(request));
937
+ for (const [key, value] of Object.entries(await this.shareOnce(request))) {
938
+ if (value instanceof OnceProp) inertia2.share(key, value);
939
+ else inertia2.shareOnce(key, value);
940
+ }
941
+ inertia2.setRootView(this.rootView(request));
942
+ const resolver = this.urlResolver();
943
+ if (resolver !== void 0) inertia2.resolveUrlUsing(resolver);
944
+ if (this.withoutSsr.length > 0) withoutSsr(request, this.withoutSsr);
945
+ let response = await next(request);
946
+ response = rebuild(response, { headers: { Vary: "X-Inertia" } });
947
+ const redirect = isRedirect(response);
948
+ const session = optionalSession();
949
+ if (redirect && session !== void 0) this.reflash(session);
950
+ if (!isInertiaRequest(request)) return response;
951
+ if (session !== void 0 && this.shouldStoreCurrentUrl(request, response)) session.setPreviousUrl(request.fullUrl());
952
+ if (request.method() === "GET" && request.header(Header.VERSION, "") !== await inertia2.getVersion()) {
953
+ response = await this.onVersionChange(request, response);
954
+ }
955
+ if (isEmpty(response)) response = await this.onEmptyResponse(request, response);
956
+ if (response.status === 302 && ["PUT", "PATCH", "DELETE"].includes(request.method())) response = rebuild(response, { status: 303 });
957
+ if (redirect && (response.headers.get("Location") ?? "").includes("#") && !isPrefetch(request)) {
958
+ response = await this.onRedirectWithFragment(request, response);
959
+ }
960
+ return response;
961
+ }
962
+ /** The session's flashed errors ({ [bag]: { field: string[] } }), shaped for the client. */
963
+ resolveValidationErrors(request) {
964
+ const flashed = optionalSession()?.get("errors", void 0);
965
+ if (flashed === void 0 || flashed === null || typeof flashed !== "object") return {};
966
+ const bags = Object.fromEntries(
967
+ Object.entries(flashed).map(([bag, fields]) => [
968
+ bag,
969
+ Object.fromEntries(
970
+ Object.entries(fields ?? {}).map(([field, messages]) => {
971
+ const list = Array.isArray(messages) ? messages.map(String) : [String(messages)];
972
+ return [field, this.withAllErrors ? list : list[0] ?? ""];
973
+ })
974
+ )
975
+ ])
976
+ );
977
+ const errorBag = request.header(Header.ERROR_BAG);
978
+ if (bags.default !== void 0 && errorBag !== void 0 && errorBag !== "") return { [errorBag]: bags.default };
979
+ if (bags.default !== void 0) return bags.default;
980
+ return bags;
981
+ }
982
+ /** inertia.store_previous_url: Inertia GET visits (not prefetches or same-page partial reloads) become previous(). */
983
+ shouldStoreCurrentUrl(request, _response) {
984
+ const config = this.container.bound(ConfigRepository3) ? this.container.make(ConfigRepository3) : void 0;
985
+ if (config?.get("inertia.store_previous_url", false) !== true) return false;
986
+ if (request.method() !== "GET" || !request.ajax() || isPrefetch(request)) return false;
987
+ const partial = request.header(Header.PARTIAL_COMPONENT);
988
+ return partial === void 0 || partial === "";
989
+ }
990
+ async onEmptyResponse(request, _response) {
991
+ return this.container.make(Redirector2).back().toResponse(request);
992
+ }
993
+ async onRedirectWithFragment(_request, response) {
994
+ return new Response(null, { status: 409, headers: { [Header.REDIRECT]: response.headers.get("Location") ?? "" } });
995
+ }
996
+ async onVersionChange(request, _response) {
997
+ optionalSession()?.reflash();
998
+ const inertia2 = this.container.make(InertiaFactory2);
999
+ const response = inertia2.location(request.fullUrl());
1000
+ return rebuild(response, { headers: { [Header.VERSION]: await inertia2.getVersion() } });
1001
+ }
1002
+ /** Keeps Inertia flash data for the page the redirect leads to. */
1003
+ reflash(session) {
1004
+ const flashed = session.get(SessionKey.FLASH_DATA, void 0);
1005
+ if (flashed !== void 0 && flashed !== null && Object.keys(flashed).length > 0) session.flash(SessionKey.FLASH_DATA, flashed);
1006
+ }
1007
+ };
1008
+ function isEmpty(response) {
1009
+ if (response.status === 204) return true;
1010
+ return response.status === 200 && (response.body === null || response.headers.get("content-length") === "0");
1011
+ }
1012
+ function md5(value) {
1013
+ return createHash("md5").update(value).digest("hex");
1014
+ }
1015
+
1016
+ // EncryptHistory.ts
1017
+ import { Container as Container2, InertiaFactory as InertiaFactory3 } from "@orieljs/contracts";
1018
+ import { inject as inject2 } from "@orieljs/container";
1019
+ var EncryptHistory = class {
1020
+ #container = inject2(Container2);
1021
+ async handle(request, next) {
1022
+ this.#container.make(InertiaFactory3).encryptHistory();
1023
+ return next(request);
1024
+ }
1025
+ };
1026
+
1027
+ // InertiaServiceProvider.ts
1028
+ import { ConsoleKernel, InertiaFactory as InertiaFactory4, InertiaSsrGateway as InertiaSsrGateway2, Router as Router2 } from "@orieljs/contracts";
1029
+ import { ServiceProvider } from "@orieljs/support";
1030
+
1031
+ // commands.ts
1032
+ import { spawn } from "child_process";
1033
+ import { existsSync as existsSync3 } from "fs";
1034
+ import { mkdir, writeFile } from "fs/promises";
1035
+ import { dirname, isAbsolute, join as join2, relative } from "path";
1036
+ import { ConfigRepository as ConfigRepository4, Router } from "@orieljs/contracts";
1037
+
1038
+ // routes.ts
1039
+ import { Str as Str2 } from "@orieljs/support";
1040
+ var PARAMETER = /\{([A-Za-z_][A-Za-z0-9_]*)(?::([A-Za-z_][A-Za-z0-9_]*))?(\?)?\}/g;
1041
+ function routeParameters(uri) {
1042
+ return [...uri.matchAll(PARAMETER)].map((match) => ({ name: match[1], optional: match[3] === "?", field: match[2] }));
1043
+ }
1044
+ function routeTable(routes, except = []) {
1045
+ const table = {};
1046
+ const named = routes.map((route) => ({ route, name: route.getName() })).filter((entry) => entry.name !== void 0 && entry.name !== "").filter(({ name }) => !except.some((pattern) => Str2.is(pattern, name))).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
1047
+ for (const { route, name } of named) table[name] = { uri: route.uri, methods: [...route.methods] };
1048
+ return table;
1049
+ }
1050
+ var ESCAPES = { "\\": "\\\\", "'": "\\'", "\n": "\\n", "\r": "\\r", "\u2028": "\\u2028", "\u2029": "\\u2029" };
1051
+ var quote = (value) => `'${value.replace(/[\\\u0027\n\r\u2028\u2029]/g, (char) => ESCAPES[char])}'`;
1052
+ function parameterType(uri) {
1053
+ const parameters = routeParameters(uri);
1054
+ if (parameters.length === 0) return "Record<never, never>";
1055
+ return `{ ${parameters.map(({ name, optional }) => `${name}${optional ? "?" : ""}: RouteParameter`).join("; ")} }`;
1056
+ }
1057
+ function renderRoutesModule(table, clientModule = "@orieljs/inertia-client") {
1058
+ const names = Object.keys(table);
1059
+ const entries = names.map((name) => {
1060
+ const { uri, methods } = table[name];
1061
+ return ` ${quote(name)}: { uri: ${quote(uri)}, methods: [${methods.map(quote).join(", ")}] },`;
1062
+ });
1063
+ const types = names.map((name) => ` ${quote(name)}: ${parameterType(table[name].uri)}`);
1064
+ return [
1065
+ "// Generated by `oriel route:types`. Do not edit: run `oriel route:types` again.",
1066
+ // Built by concatenation so tools that scan source for imports don't mistake this template for one.
1067
+ `import { defineRoutes, type RouteParameter } from ${quote(clientModule)}`,
1068
+ "",
1069
+ `export const routes = defineRoutes({${entries.length === 0 ? "" : `
1070
+ ${entries.join("\n")}
1071
+ `}})`,
1072
+ "",
1073
+ `declare module ${quote(clientModule)} {`,
1074
+ " interface RouteList {",
1075
+ ...types,
1076
+ " }",
1077
+ "}",
1078
+ ""
1079
+ ].join("\n");
1080
+ }
1081
+ function renderServerRouteTypes(table) {
1082
+ const members = Object.keys(table).map((name) => ` ${quote(name)}: ${parameterType(table[name].uri).replace(/RouteParameter/g, "unknown")}`);
1083
+ const block = (module) => [`declare module ${quote(module)} {`, " interface RouteList {", ...members, " }", "}"];
1084
+ return [
1085
+ "// Generated by `oriel route:types`. Do not edit: run `oriel route:types` again.",
1086
+ "",
1087
+ ...block("@orieljs/framework"),
1088
+ "",
1089
+ ...block("@orieljs/framework/contracts"),
1090
+ "",
1091
+ "export {}",
1092
+ ""
1093
+ ].join("\n");
1094
+ }
1095
+
1096
+ // commands.ts
1097
+ var MIDDLEWARE_STUB = `import { InertiaMiddleware } from '@orieljs/framework'
1098
+ import type { HttpRequest } from '@orieljs/framework/contracts'
1099
+
1100
+ export class {{ class }} extends InertiaMiddleware {
1101
+ /** The root view loaded on the first page visit: resources/views/app.tsx. */
1102
+ protected override rootViewName = 'app'
1103
+
1104
+ /** Props shared with every page (typed for the pages by \`oriel inertia:types\`). */
1105
+ override async share(request: HttpRequest) {
1106
+ return {
1107
+ ...(await super.share(request)),
1108
+ }
1109
+ }
1110
+ }
1111
+ `;
1112
+ async function loadPageTypes() {
1113
+ const specifier = "@orieljs/vite/types";
1114
+ try {
1115
+ return await import(
1116
+ /* @vite-ignore */
1117
+ specifier
1118
+ );
1119
+ } catch {
1120
+ return void 0;
1121
+ }
1122
+ }
1123
+ function registerInertiaCommands(kernel, app, gateway, options = {}) {
1124
+ const config = (key, fallback) => app.bound(ConfigRepository4) ? app.make(ConfigRepository4).get(key, fallback) : fallback;
1125
+ const absolute = (path) => isAbsolute(path) ? path : app.basePath(path);
1126
+ kernel.command(
1127
+ "route:types {--output= : Where to write the route module} {--server= : Where to write the server route name types}",
1128
+ async function() {
1129
+ if (!app.bound(Router)) {
1130
+ this.error("The router is not available. Is the Routing service provider registered?");
1131
+ return 1;
1132
+ }
1133
+ const output = absolute(optionText(this.option("output")) ?? config("inertia.types.routes", "resources/js/oriel/routes.ts"));
1134
+ const server = absolute(optionText(this.option("server")) ?? config("inertia.types.server_routes", "types/routes.d.ts"));
1135
+ const table = routeTable(app.make(Router).getRoutes(), config("inertia.types.except_routes", []));
1136
+ await mkdir(dirname(output), { recursive: true });
1137
+ await writeFile(output, renderRoutesModule(table));
1138
+ await mkdir(dirname(server), { recursive: true });
1139
+ await writeFile(server, renderServerRouteTypes(routeTable(app.make(Router).getRoutes())));
1140
+ this.info(`Route types for ${Object.keys(table).length} named routes written to ${relative(app.basePath(), output)}.`);
1141
+ return 0;
1142
+ }
1143
+ ).purpose("Generate the route table and route name types for the frontend");
1144
+ kernel.command("inertia:types {--output= : Where to write the page types}", async function() {
1145
+ const module = await (options.pageTypes ?? loadPageTypes)();
1146
+ if (module === void 0) {
1147
+ this.error("Generating page types needs @orieljs/vite: install it as a dev dependency.");
1148
+ return 1;
1149
+ }
1150
+ const output = absolute(optionText(this.option("output")) ?? config("inertia.types.pages", "resources/js/oriel/pages.d.ts"));
1151
+ const result = await module.generatePageTypes({ root: app.basePath(), output });
1152
+ for (const warning of result.warnings) this.warn(warning);
1153
+ this.info(`Page types for ${result.pages} pages written to ${relative(app.basePath(), result.output)}.`);
1154
+ return 0;
1155
+ }).purpose("Generate TypeScript types for the Inertia page props");
1156
+ kernel.command("inertia:middleware {name=HandleInertiaRequests : The middleware class name} {--force : Overwrite an existing file}", async function() {
1157
+ const name = String(this.argument("name") ?? "HandleInertiaRequests");
1158
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
1159
+ this.error(`Middleware name [${name}] is not a valid class name.`);
1160
+ return 1;
1161
+ }
1162
+ const file = app.appPath(join2("Http", "Middleware", `${name}.ts`));
1163
+ if (existsSync3(file) && this.option("force") !== true) {
1164
+ this.error(`Middleware [${relative(app.basePath(), file)}] already exists.`);
1165
+ return 1;
1166
+ }
1167
+ await mkdir(dirname(file), { recursive: true });
1168
+ await writeFile(file, MIDDLEWARE_STUB.replace("{{ class }}", () => name));
1169
+ this.info(`Middleware [${relative(app.basePath(), file)}] created successfully.`);
1170
+ return 0;
1171
+ }).purpose("Create a new Inertia middleware");
1172
+ kernel.command("inertia:start-ssr {--runtime= : The runtime that runs the SSR bundle (node or bun)}", async function() {
1173
+ if (!config("inertia.ssr.enabled", true)) {
1174
+ this.error("Inertia SSR is not enabled. Enable it via the `inertia.ssr.enabled` config option.");
1175
+ return 1;
1176
+ }
1177
+ const bundle = gateway.bundle();
1178
+ if (bundle === void 0) {
1179
+ this.error("Inertia SSR bundle not found. Build it with `vite build --ssr`, or set `inertia.ssr.bundle`.");
1180
+ return 1;
1181
+ }
1182
+ const runtime = optionText(this.option("runtime")) ?? config("inertia.ssr.runtime", "node");
1183
+ const child = (options.spawn ?? spawn)(runtime, [bundle], { stdio: "inherit" });
1184
+ return await new Promise((resolve) => {
1185
+ child.on("exit", (code) => resolve(code ?? 0));
1186
+ child.on("error", (error) => {
1187
+ this.error(`Could not start the SSR server: ${error.message}`);
1188
+ resolve(1);
1189
+ });
1190
+ });
1191
+ }).purpose("Start the Inertia SSR server");
1192
+ kernel.command("inertia:stop-ssr", async function() {
1193
+ try {
1194
+ await fetch(`${gateway.productionUrl()}/shutdown`);
1195
+ } catch {
1196
+ }
1197
+ this.info("Inertia SSR server stopped.");
1198
+ return 0;
1199
+ }).purpose("Stop the Inertia SSR server");
1200
+ kernel.command("inertia:check-ssr", async function() {
1201
+ if (await gateway.isHealthy()) {
1202
+ this.info("Inertia SSR server is running.");
1203
+ return 0;
1204
+ }
1205
+ this.error("Inertia SSR server is not running.");
1206
+ return 1;
1207
+ }).purpose("Check the Inertia SSR server health status");
1208
+ }
1209
+ function optionText(value) {
1210
+ return value === void 0 || value === null || value === false || value === "" ? void 0 : String(value);
1211
+ }
1212
+
1213
+ // ssr/HttpGateway.ts
1214
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
1215
+ import {
1216
+ ConfigRepository as ConfigRepository5,
1217
+ EventDispatcher
1218
+ } from "@orieljs/contracts";
1219
+ import { Str as Str3 } from "@orieljs/support";
1220
+ var SSR_RENDER_FAILED = "inertia.ssr.failed";
1221
+ var HttpGateway = class {
1222
+ constructor(app) {
1223
+ this.app = app;
1224
+ }
1225
+ app;
1226
+ #disabled;
1227
+ #except = [];
1228
+ disable(condition = true) {
1229
+ this.#disabled = condition;
1230
+ }
1231
+ /** Paths (wildcards allowed) never rendered on the server. */
1232
+ except(paths) {
1233
+ this.#except.push(...[paths].flat());
1234
+ }
1235
+ async dispatch(page) {
1236
+ if (!this.enabled(currentRequest())) return void 0;
1237
+ const hot = this.hotUrl();
1238
+ if (hot === void 0 && this.#config("inertia.ssr.ensure_bundle_exists", true) && this.bundle() === void 0) return void 0;
1239
+ const url = hot === void 0 ? `${this.productionUrl()}/render` : `${hot}/__inertia_ssr`;
1240
+ try {
1241
+ const response = await fetch(url, {
1242
+ method: "POST",
1243
+ headers: { "content-type": "application/json", accept: "application/json" },
1244
+ body: JSON.stringify(page),
1245
+ signal: this.#signal()
1246
+ });
1247
+ const data = await response.json().catch(() => null);
1248
+ if (!response.ok) {
1249
+ await this.#failed({ page, error: String(data?.error ?? `SSR server responded with ${response.status}`), type: String(data?.type ?? "render"), ...details(data) });
1250
+ return void 0;
1251
+ }
1252
+ if (data === null) return void 0;
1253
+ return { head: (data.head ?? []).join("\n"), body: data.body ?? "" };
1254
+ } catch (error) {
1255
+ if (error instanceof SsrError) throw error;
1256
+ await this.#failed({ page, error: error instanceof Error ? error.message : String(error), type: "connection" });
1257
+ return void 0;
1258
+ }
1259
+ }
1260
+ /** GET {inertia.ssr.url}/health: any 2xx is healthy. */
1261
+ async isHealthy() {
1262
+ try {
1263
+ return (await fetch(`${this.productionUrl()}/health`, { signal: this.#signal() })).ok;
1264
+ } catch {
1265
+ return false;
1266
+ }
1267
+ }
1268
+ productionUrl() {
1269
+ return this.#config("inertia.ssr.url", "http://127.0.0.1:13714").replace(/\/+$/, "");
1270
+ }
1271
+ /** inertia.ssr.hot_url, else the Vite dev server URL in public/hot; undefined when Vite isn't running. */
1272
+ hotUrl() {
1273
+ const hotFile = this.app.publicPath("hot");
1274
+ if (!existsSync4(hotFile)) return void 0;
1275
+ const configured = this.#config("inertia.ssr.hot_url", null);
1276
+ const url = configured !== null && configured !== "" ? configured : readFileSync2(hotFile, "utf8");
1277
+ return url.trim().replace(/\/+$/, "");
1278
+ }
1279
+ /** inertia.ssr.bundle, else bootstrap/ssr/{ssr,app}.{js,mjs}, else public/js/{ssr,app}.js (BundleDetector). */
1280
+ bundle() {
1281
+ const configured = this.#config("inertia.ssr.bundle", null);
1282
+ const candidates = [
1283
+ configured,
1284
+ this.app.basePath("bootstrap/ssr/ssr.js"),
1285
+ this.app.basePath("bootstrap/ssr/app.js"),
1286
+ this.app.basePath("bootstrap/ssr/ssr.mjs"),
1287
+ this.app.basePath("bootstrap/ssr/app.mjs"),
1288
+ this.app.publicPath("js/ssr.js"),
1289
+ this.app.publicPath("js/app.js")
1290
+ ];
1291
+ return candidates.find((path) => typeof path === "string" && path !== "" && existsSync4(path));
1292
+ }
1293
+ enabled(request) {
1294
+ const disabled = this.#disabled;
1295
+ const enabled = disabled !== void 0 ? !(typeof disabled === "function" ? disabled() : disabled) : this.#config("inertia.ssr.enabled", true);
1296
+ if (!enabled) return false;
1297
+ if (request === void 0) return true;
1298
+ if (isExcludedFromSsr(request)) return false;
1299
+ return !this.#except.some((pattern) => {
1300
+ const trimmed = pattern === "/" ? pattern : pattern.replace(/^\/+|\/+$/g, "");
1301
+ return Str3.is(trimmed, request.fullUrl()) || request.is(trimmed);
1302
+ });
1303
+ }
1304
+ async #failed(failure) {
1305
+ const scope = requestScope() ?? this.app;
1306
+ if (scope.bound(EventDispatcher)) await scope.make(EventDispatcher).dispatch(SSR_RENDER_FAILED, [failure]);
1307
+ if (this.#config("inertia.ssr.throw_on_error", false)) throw new SsrError(`SSR render failed: ${failure.error}`);
1308
+ }
1309
+ #signal() {
1310
+ const seconds = this.#config("inertia.ssr.timeout", null);
1311
+ return typeof seconds === "number" && seconds > 0 ? AbortSignal.timeout(seconds * 1e3) : void 0;
1312
+ }
1313
+ #config(key, fallback) {
1314
+ return this.app.bound(ConfigRepository5) ? this.app.make(ConfigRepository5).get(key, fallback) : fallback;
1315
+ }
1316
+ };
1317
+ function details(data) {
1318
+ const text = (key) => typeof data?.[key] === "string" ? data[key] : void 0;
1319
+ return { hint: text("hint"), browserApi: text("browserApi"), stack: text("stack"), sourceLocation: text("sourceLocation") };
1320
+ }
1321
+
1322
+ // InertiaServiceProvider.ts
1323
+ var InertiaServiceProvider = class extends ServiceProvider {
1324
+ register() {
1325
+ this.mergeConfigFrom(
1326
+ {
1327
+ ssr: {
1328
+ enabled: true,
1329
+ runtime: "node",
1330
+ url: "http://127.0.0.1:13714",
1331
+ hot_url: null,
1332
+ timeout: null,
1333
+ ensure_bundle_exists: true,
1334
+ bundle: null,
1335
+ throw_on_error: false
1336
+ },
1337
+ pages: {
1338
+ ensure_pages_exist: false,
1339
+ paths: [this.app.resourcePath("js/pages")],
1340
+ extensions: ["js", "jsx", "svelte", "ts", "tsx", "vue", "component.ts"]
1341
+ },
1342
+ testing: { ensure_pages_exist: true },
1343
+ expose_shared_prop_keys: true,
1344
+ store_previous_url: false,
1345
+ history: { encrypt: false },
1346
+ types: { pages: "resources/js/oriel/pages.d.ts", routes: "resources/js/oriel/routes.ts", server_routes: "types/routes.d.ts", except_routes: [] }
1347
+ },
1348
+ "inertia"
1349
+ );
1350
+ this.app.singleton(ResponseFactory, () => new ResponseFactory(this.app));
1351
+ this.app.scoped(InertiaFactory4, (container) => container.make(ResponseFactory).forRequest());
1352
+ this.app.singleton(HttpGateway, () => new HttpGateway(this.app));
1353
+ this.app.singleton(InertiaSsrGateway2, (container) => container.make(HttpGateway));
1354
+ }
1355
+ boot() {
1356
+ if (this.app.bound(Router2)) this.app.make(Router2).aliasMiddleware("inertia.encrypt", EncryptHistory);
1357
+ if (this.app.bound(ConsoleKernel)) registerInertiaCommands(this.app.make(ConsoleKernel), this.app, this.app.make(HttpGateway));
1358
+ }
1359
+ };
1360
+ export {
1361
+ AlwaysProp,
1362
+ ComponentNotFoundError,
1363
+ DeferProp,
1364
+ EncryptHistory,
1365
+ Header,
1366
+ HttpGateway,
1367
+ Inertia,
1368
+ InertiaError,
1369
+ InertiaHtml,
1370
+ InertiaMiddleware,
1371
+ Response2 as InertiaPageResponse,
1372
+ ResponseFactory as InertiaResponseFactory,
1373
+ InertiaServiceProvider,
1374
+ MIDDLEWARE_STUB,
1375
+ MergeProp,
1376
+ MergeableProp,
1377
+ OnceProp,
1378
+ OptionalProp,
1379
+ Prop,
1380
+ PropsResolver,
1381
+ SSR_RENDER_FAILED,
1382
+ ScrollProp,
1383
+ SessionKey,
1384
+ SsrError,
1385
+ currentRequest,
1386
+ defaultUrl,
1387
+ inertia,
1388
+ inertiaBody,
1389
+ inertiaFactory,
1390
+ inertiaHead,
1391
+ isExcludedFromSsr,
1392
+ isInertiaRequest,
1393
+ isPrefetch,
1394
+ isProp,
1395
+ isRedirect,
1396
+ optionalSession,
1397
+ pageExists,
1398
+ pageJson,
1399
+ pageMarkup,
1400
+ parseList,
1401
+ rebuild,
1402
+ registerInertiaCommands,
1403
+ renderRoutesModule,
1404
+ renderServerRouteTypes,
1405
+ requestScope,
1406
+ routeParameters,
1407
+ routeTable,
1408
+ scrollMetadataFrom,
1409
+ secondsUntil,
1410
+ withoutSsr
1411
+ };
1412
+ //# sourceMappingURL=index.js.map