@gravito/ion 3.1.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -65,8 +65,71 @@ var InertiaService = class {
65
65
  this.onRenderCallback = config.onRender;
66
66
  }
67
67
  sharedProps = {};
68
+ errorBags = {};
69
+ encryptHistoryFlag = false;
70
+ clearHistoryFlag = false;
68
71
  logLevel;
69
72
  onRenderCallback;
73
+ /**
74
+ * Creates a deferred prop that will be loaded after the initial render.
75
+ *
76
+ * @param factory - Function that resolves to the prop value.
77
+ * @param group - Optional group name for organizing deferred props.
78
+ * @returns A deferred prop definition.
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * props: {
83
+ * user: { id: 1 },
84
+ * stats: InertiaService.defer(() => fetchStats())
85
+ * }
86
+ * ```
87
+ */
88
+ static defer(factory, group = "default") {
89
+ return {
90
+ _type: "deferred",
91
+ factory,
92
+ group
93
+ };
94
+ }
95
+ /**
96
+ * Marks a prop to be merged (shallow) with existing data during partial reloads.
97
+ *
98
+ * @param value - The prop value to merge.
99
+ * @param matchOn - Optional key(s) to match on when merging arrays.
100
+ * @returns A merge prop definition.
101
+ */
102
+ static merge(value, matchOn) {
103
+ return {
104
+ _type: "merge",
105
+ value,
106
+ matchOn
107
+ };
108
+ }
109
+ /**
110
+ * Marks a prop to prepend to an array during partial reloads.
111
+ *
112
+ * @param value - The prop value to prepend.
113
+ * @returns A prepend prop definition.
114
+ */
115
+ static prepend(value) {
116
+ return {
117
+ _type: "prepend",
118
+ value
119
+ };
120
+ }
121
+ /**
122
+ * Marks a prop to be deep-merged with existing data during partial reloads.
123
+ *
124
+ * @param value - The prop value to deep merge.
125
+ * @returns A deep merge prop definition.
126
+ */
127
+ static deepMerge(value) {
128
+ return {
129
+ _type: "deepMerge",
130
+ value
131
+ };
132
+ }
70
133
  /**
71
134
  * Internal logging helper that respects the configured log level.
72
135
  *
@@ -154,10 +217,14 @@ var InertiaService = class {
154
217
  const partialData = this.context.req.header("X-Inertia-Partial-Data");
155
218
  const partialExcept = this.context.req.header("X-Inertia-Partial-Except");
156
219
  const partialComponent = this.context.req.header("X-Inertia-Partial-Component");
220
+ const resetKeys = this.context.req.header("X-Inertia-Reset");
157
221
  const isPartial = partialComponent === component;
158
222
  const only = partialData && isPartial ? partialData.split(",") : [];
159
223
  const except = partialExcept && isPartial ? partialExcept.split(",") : [];
224
+ const resetKeyList = resetKeys ? resetKeys.split(",") : [];
160
225
  const resolved = {};
226
+ const deferredGroups2 = {};
227
+ const mergedKeys2 = [];
161
228
  for (const [key, value] of Object.entries(p)) {
162
229
  if (only.length > 0 && !only.includes(key)) {
163
230
  continue;
@@ -165,6 +232,33 @@ var InertiaService = class {
165
232
  if (except.length > 0 && except.includes(key)) {
166
233
  continue;
167
234
  }
235
+ if (value && typeof value === "object" && "_type" in value && value._type === "deferred") {
236
+ const deferred = value;
237
+ const group = deferred.group ?? "default";
238
+ if (!deferredGroups2[group]) {
239
+ deferredGroups2[group] = [];
240
+ }
241
+ deferredGroups2[group].push(key);
242
+ continue;
243
+ }
244
+ if (value && typeof value === "object" && "_type" in value && ["merge", "prepend", "deepMerge"].includes(value._type)) {
245
+ const merged = value;
246
+ const existing = mergedKeys2.find((m) => m.mode === merged._type);
247
+ if (existing) {
248
+ existing.keys.push(key);
249
+ } else {
250
+ mergedKeys2.push({
251
+ keys: [key],
252
+ mode: merged._type
253
+ });
254
+ }
255
+ resolved[key] = merged.value;
256
+ continue;
257
+ }
258
+ if (resetKeyList.includes(key)) {
259
+ resolved[key] = void 0;
260
+ continue;
261
+ }
168
262
  if (typeof value === "function") {
169
263
  const propStart = performance.now();
170
264
  try {
@@ -180,7 +274,7 @@ var InertiaService = class {
180
274
  resolved[key] = value;
181
275
  }
182
276
  }
183
- return resolved;
277
+ return { resolved, deferredGroups: deferredGroups2, mergedKeys: mergedKeys2 };
184
278
  };
185
279
  const resolveVersion = async () => {
186
280
  if (typeof this.config.version === "function") {
@@ -196,12 +290,32 @@ var InertiaService = class {
196
290
  }
197
291
  return { ...this.sharedProps, ...propsToMerge };
198
292
  };
293
+ const {
294
+ resolved: resolvedPropsData,
295
+ deferredGroups,
296
+ mergedKeys
297
+ } = await resolveProps(getMergedProps());
199
298
  const page = {
200
299
  component,
201
- props: await resolveProps(getMergedProps()),
300
+ props: resolvedPropsData,
202
301
  url: pageUrl,
203
302
  version
204
303
  };
304
+ if (Object.keys(deferredGroups).length > 0) {
305
+ page.deferredProps = deferredGroups;
306
+ }
307
+ if (mergedKeys.length > 0) {
308
+ page.mergeProps = mergedKeys;
309
+ }
310
+ if (this.encryptHistoryFlag) {
311
+ page.encryptHistory = true;
312
+ }
313
+ if (this.clearHistoryFlag) {
314
+ page.clearHistory = true;
315
+ }
316
+ if (Object.keys(this.errorBags).length > 0) {
317
+ page.errorBags = this.errorBags;
318
+ }
205
319
  let pageJson;
206
320
  try {
207
321
  pageJson = JSON.stringify(page);
@@ -290,6 +404,42 @@ var InertiaService = class {
290
404
  duration: `${duration.toFixed(2)}ms`,
291
405
  error
292
406
  });
407
+ const isDev = process.env.NODE_ENV !== "production";
408
+ if (isDev && error instanceof Error) {
409
+ const errorHtml = `
410
+ <!DOCTYPE html>
411
+ <html>
412
+ <head>
413
+ <meta charset="utf-8">
414
+ <meta name="viewport" content="width=device-width, initial-scale=1">
415
+ <title>Inertia Render Error</title>
416
+ <style>
417
+ * { margin: 0; padding: 0; box-sizing: border-box; }
418
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto; background: #f5f5f5; padding: 20px; }
419
+ .container { max-width: 800px; margin: 40px auto; background: white; border-radius: 8px; padding: 30px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
420
+ h1 { color: #d32f2f; margin-bottom: 10px; font-size: 24px; }
421
+ .meta { color: #666; margin-bottom: 20px; font-size: 14px; }
422
+ .component { background: #f5f5f5; padding: 10px 15px; border-left: 3px solid #1976d2; margin-bottom: 20px; font-family: monospace; }
423
+ .error-message { background: #ffebee; border: 1px solid #ef5350; border-radius: 4px; padding: 15px; margin-bottom: 20px; color: #c62828; }
424
+ .stack { background: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; padding: 15px; overflow-x: auto; font-family: monospace; font-size: 12px; color: #333; line-height: 1.5; }
425
+ .hint { background: #e8f5e9; border-left: 3px solid #4caf50; padding: 15px; margin-top: 20px; color: #2e7d32; }
426
+ </style>
427
+ </head>
428
+ <body>
429
+ <div class="container">
430
+ <h1>\u{1F6A8} Inertia Render Error</h1>
431
+ <div class="meta">Component: <span class="component">${component}</span></div>
432
+ <div class="error-message">${error.message || "Unknown error"}</div>
433
+ ${error.stack ? `<div><strong>Stack Trace:</strong><div class="stack">${error.stack.split("\n").map((line) => line.replace(/</g, "&lt;").replace(/>/g, "&gt;")).join("\n")}</div></div>` : ""}
434
+ <div class="hint">
435
+ <strong>\u{1F4A1} Dev Mode Tip:</strong> This enhanced error page is only visible in development mode. In production, a generic error message is shown.
436
+ </div>
437
+ </div>
438
+ </body>
439
+ </html>
440
+ `;
441
+ return this.context.html(errorHtml, 500);
442
+ }
293
443
  return new Response("Inertia Render Error", { status: 500 });
294
444
  }
295
445
  }
@@ -330,7 +480,7 @@ var InertiaService = class {
330
480
  * ```
331
481
  */
332
482
  shareAll(props) {
333
- Object.assign(this.sharedProps, props);
483
+ this.sharedProps = { ...this.sharedProps, ...props };
334
484
  }
335
485
  /**
336
486
  * Returns a shallow copy of the current shared props.
@@ -350,6 +500,94 @@ var InertiaService = class {
350
500
  getSharedProps() {
351
501
  return { ...this.sharedProps };
352
502
  }
503
+ /**
504
+ * Instructs the Inertia client to navigate to a different URL.
505
+ *
506
+ * For Inertia AJAX requests, this returns a 409 Conflict with the `X-Inertia-Location` header.
507
+ * For regular page loads, this returns a 302 Found redirect.
508
+ *
509
+ * This is useful for server-side redirects triggered by authentication or authorization checks.
510
+ *
511
+ * @param url - The URL to redirect to.
512
+ * @returns A redirect response.
513
+ *
514
+ * @example
515
+ * ```typescript
516
+ * if (!ctx.get('user')) {
517
+ * return inertia.location('/login');
518
+ * }
519
+ * ```
520
+ */
521
+ location(url) {
522
+ const isInertiaRequest = Boolean(this.context.req.header("X-Inertia"));
523
+ if (isInertiaRequest) {
524
+ this.context.header("X-Inertia-Location", url);
525
+ return new Response("", { status: 409 });
526
+ }
527
+ this.context.header("Location", url);
528
+ return new Response("", { status: 302 });
529
+ }
530
+ /**
531
+ * Controls whether the Inertia client should encrypt browser history.
532
+ *
533
+ * When enabled, history is not written to the browser's History API,
534
+ * preventing users from using the browser back button to return to previous pages.
535
+ *
536
+ * @param encrypt - Whether to encrypt history (defaults to true).
537
+ * @returns The service instance for method chaining.
538
+ *
539
+ * @example
540
+ * ```typescript
541
+ * return await inertia.encryptHistory(true).render('SecurePage');
542
+ * ```
543
+ */
544
+ encryptHistory(encrypt = true) {
545
+ this.encryptHistoryFlag = encrypt;
546
+ return this;
547
+ }
548
+ /**
549
+ * Clears the browser history after the page load.
550
+ *
551
+ * Useful for sensitive operations or multi-step wizards where you don't want
552
+ * users navigating back to previous states.
553
+ *
554
+ * @returns The service instance for method chaining.
555
+ *
556
+ * @example
557
+ * ```typescript
558
+ * return await inertia.clearHistory().render('SuccessPage');
559
+ * ```
560
+ */
561
+ clearHistory() {
562
+ this.clearHistoryFlag = true;
563
+ return this;
564
+ }
565
+ /**
566
+ * Registers form validation errors organized into named bags.
567
+ *
568
+ * Error bags allow multiple validation failure scenarios to coexist.
569
+ * For example, you might have 'default' errors and 'import' errors from different forms.
570
+ *
571
+ * @param errors - Validation errors, with field names as keys and error messages as values.
572
+ * @param bag - The error bag name (defaults to 'default').
573
+ * @returns The service instance for method chaining.
574
+ *
575
+ * @example
576
+ * ```typescript
577
+ * inertia.withErrors({
578
+ * email: 'Email is required',
579
+ * password: 'Must be 8+ characters'
580
+ * }, 'login');
581
+ *
582
+ * inertia.withErrors({
583
+ * line_1: 'Invalid CSV format'
584
+ * }, 'import');
585
+ * ```
586
+ */
587
+ withErrors(errors, bag = "default") {
588
+ this.errorBags[bag] = errors;
589
+ return this;
590
+ }
353
591
  };
354
592
 
355
593
  // src/index.ts
@@ -381,9 +619,35 @@ var OrbitIon = class {
381
619
  core.logger.info("\u{1F6F0}\uFE0F Orbit Inertia installed (Callable Interface)");
382
620
  const appVersion = this.options.version ?? core.config.get("APP_VERSION", "1.0.0");
383
621
  const rootView = this.options.rootView ?? "app";
622
+ const csrfEnabled = this.options.csrf?.enabled !== false;
623
+ const csrfCookieName = this.options.csrf?.cookieName ?? "XSRF-TOKEN";
624
+ const VERSION_CACHE_TTL = 6e4;
625
+ let cachedVersion;
626
+ let versionCacheTime = 0;
627
+ const resolveAppVersion = async () => {
628
+ if (typeof appVersion !== "function") {
629
+ return appVersion;
630
+ }
631
+ const now = Date.now();
632
+ if (cachedVersion && now - versionCacheTime < VERSION_CACHE_TTL) {
633
+ return cachedVersion;
634
+ }
635
+ cachedVersion = await appVersion();
636
+ versionCacheTime = now;
637
+ return cachedVersion;
638
+ };
639
+ if (csrfEnabled) {
640
+ core.adapter.use("*", async (c, next) => {
641
+ const token = crypto.randomUUID();
642
+ const secure = process.env.NODE_ENV === "production";
643
+ const cookieValue = `${csrfCookieName}=${token}; Path=/; ${secure ? "Secure; " : ""}SameSite=Lax`;
644
+ c.header("Set-Cookie", cookieValue);
645
+ return await next();
646
+ });
647
+ }
384
648
  core.adapter.use("*", async (c, next) => {
385
649
  const service = new InertiaService(c, {
386
- version: appVersion,
650
+ version: resolveAppVersion,
387
651
  rootView,
388
652
  ssr: this.options.ssr
389
653
  });
@@ -395,6 +659,10 @@ var OrbitIon = class {
395
659
  shareAll: service.shareAll.bind(service),
396
660
  getSharedProps: service.getSharedProps.bind(service),
397
661
  render: service.render.bind(service),
662
+ location: service.location.bind(service),
663
+ encryptHistory: service.encryptHistory.bind(service),
664
+ clearHistory: service.clearHistory.bind(service),
665
+ withErrors: service.withErrors.bind(service),
398
666
  service
399
667
  });
400
668
  c.set("inertia", inertiaProxy);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gravito/ion",
3
- "version": "3.1.0",
4
- "description": "Inertia.js adapter for Gravito",
3
+ "version": "4.0.0",
4
+ "description": "Inertia.js v2 adapter for Gravito",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
7
7
  "type": "module",