@gravito/ion 3.1.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -4
- package/README.zh-TW.md +81 -4
- package/dist/index.cjs +272 -4
- package/dist/index.d.cts +289 -1
- package/dist/index.d.ts +289 -1
- package/dist/index.js +272 -4
- package/package.json +6 -6
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:
|
|
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, "<").replace(/>/g, ">")).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
|
-
|
|
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:
|
|
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": "
|
|
4
|
-
"description": "Inertia.js adapter for Gravito",
|
|
3
|
+
"version": "4.0.1",
|
|
4
|
+
"description": "Inertia.js v2 adapter for Gravito",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.js",
|
|
7
7
|
"type": "module",
|
|
@@ -28,13 +28,13 @@
|
|
|
28
28
|
"test:integration": "test $(find tests -name '*.integration.test.ts' 2>/dev/null | wc -l) -gt 0 && find tests -name '*.integration.test.ts' -print0 | xargs -0 bun test --timeout=10000 || echo 'No integration tests found'"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@gravito/core": "
|
|
32
|
-
"@gravito/photon": "
|
|
31
|
+
"@gravito/core": "^1.6.1",
|
|
32
|
+
"@gravito/photon": "^1.0.1"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"bun-types": "latest",
|
|
36
|
-
"@gravito/core": "
|
|
37
|
-
"@gravito/photon": "
|
|
36
|
+
"@gravito/core": "^1.6.1",
|
|
37
|
+
"@gravito/photon": "^1.0.1",
|
|
38
38
|
"tsup": "^8.5.1",
|
|
39
39
|
"typescript": "^5.9.3"
|
|
40
40
|
},
|