@gravito/ion 3.0.1 → 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/README.md +131 -40
- package/README.zh-TW.md +172 -16
- package/dist/index.cjs +600 -81
- package/dist/index.d.cts +625 -90
- package/dist/index.d.ts +625 -90
- package/dist/index.js +596 -81
- package/package.json +8 -6
package/dist/index.js
CHANGED
|
@@ -1,158 +1,669 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var InertiaError = class extends Error {
|
|
3
|
+
constructor(code, httpStatus = 500, details) {
|
|
4
|
+
super(code);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.httpStatus = httpStatus;
|
|
7
|
+
this.details = details;
|
|
8
|
+
this.name = "InertiaError";
|
|
9
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
10
|
+
Error.captureStackTrace?.(this, this.constructor);
|
|
11
|
+
}
|
|
12
|
+
toJSON() {
|
|
13
|
+
return {
|
|
14
|
+
name: this.name,
|
|
15
|
+
code: this.code,
|
|
16
|
+
httpStatus: this.httpStatus,
|
|
17
|
+
message: this.message,
|
|
18
|
+
details: this.details
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var InertiaConfigError = class extends InertiaError {
|
|
23
|
+
constructor(message, details) {
|
|
24
|
+
super("CONFIG_ERROR", 500, {
|
|
25
|
+
...details,
|
|
26
|
+
message,
|
|
27
|
+
hint: details?.hint ?? "Check your OrbitIon configuration and ensure dependencies are loaded."
|
|
28
|
+
});
|
|
29
|
+
this.name = "InertiaConfigError";
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var InertiaDataError = class extends InertiaError {
|
|
33
|
+
constructor(message, details) {
|
|
34
|
+
super("DATA_ERROR", 500, {
|
|
35
|
+
...details,
|
|
36
|
+
message,
|
|
37
|
+
hint: details?.hint ?? "Ensure all props are JSON-serializable (no circular refs, BigInt, or functions)."
|
|
38
|
+
});
|
|
39
|
+
this.name = "InertiaDataError";
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
var InertiaTemplateError = class extends InertiaError {
|
|
43
|
+
constructor(message, details) {
|
|
44
|
+
super("TEMPLATE_ERROR", 500, {
|
|
45
|
+
...details,
|
|
46
|
+
message,
|
|
47
|
+
hint: details?.hint ?? "Check if the root view template exists and is properly configured."
|
|
48
|
+
});
|
|
49
|
+
this.name = "InertiaTemplateError";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
1
53
|
// src/InertiaService.ts
|
|
2
54
|
var InertiaService = class {
|
|
3
55
|
/**
|
|
4
|
-
*
|
|
56
|
+
* Initializes a new instance of the Inertia service.
|
|
5
57
|
*
|
|
6
|
-
* @param context - The Gravito request context
|
|
7
|
-
* @param config -
|
|
58
|
+
* @param context - The current Gravito request context.
|
|
59
|
+
* @param config - Instance configuration options.
|
|
8
60
|
*/
|
|
9
61
|
constructor(context, config = {}) {
|
|
10
62
|
this.context = context;
|
|
11
63
|
this.config = config;
|
|
64
|
+
this.logLevel = config.logLevel ?? "info";
|
|
65
|
+
this.onRenderCallback = config.onRender;
|
|
12
66
|
}
|
|
13
67
|
sharedProps = {};
|
|
68
|
+
errorBags = {};
|
|
69
|
+
encryptHistoryFlag = false;
|
|
70
|
+
clearHistoryFlag = false;
|
|
71
|
+
logLevel;
|
|
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
|
+
}
|
|
14
95
|
/**
|
|
15
|
-
*
|
|
96
|
+
* Marks a prop to be merged (shallow) with existing data during partial reloads.
|
|
16
97
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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.
|
|
20
111
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
|
|
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.
|
|
24
123
|
*
|
|
25
|
-
* @param value - The
|
|
26
|
-
* @returns
|
|
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
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Internal logging helper that respects the configured log level.
|
|
135
|
+
*
|
|
136
|
+
* Routes logs to the Gravito context logger if available.
|
|
137
|
+
*
|
|
138
|
+
* @param level - Log severity level.
|
|
139
|
+
* @param message - Descriptive message.
|
|
140
|
+
* @param data - Optional metadata for the log.
|
|
141
|
+
*/
|
|
142
|
+
log(level, message, data) {
|
|
143
|
+
const levels = ["debug", "info", "warn", "error", "silent"];
|
|
144
|
+
const currentLevelIndex = levels.indexOf(this.logLevel);
|
|
145
|
+
const messageLevelIndex = levels.indexOf(level);
|
|
146
|
+
if (this.logLevel === "silent" || messageLevelIndex < currentLevelIndex) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const logger = typeof this.context.get === "function" ? this.context.get("logger") : void 0;
|
|
150
|
+
if (logger && typeof logger[level] === "function") {
|
|
151
|
+
logger[level](message, data);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Escapes a string for safe embedding into a single-quoted HTML attribute.
|
|
156
|
+
*
|
|
157
|
+
* This ensures that JSON strings can be safely passed to the frontend
|
|
158
|
+
* via the `data-page` attribute without breaking the HTML structure or
|
|
159
|
+
* introducing XSS vulnerabilities.
|
|
160
|
+
*
|
|
161
|
+
* @param value - Raw JSON or text string.
|
|
162
|
+
* @returns Safely escaped HTML attribute value.
|
|
27
163
|
*/
|
|
28
164
|
escapeForSingleQuotedHtmlAttribute(value) {
|
|
29
165
|
return value.replace(/&/g, "&").replace(/\\"/g, "\\"").replace(/</g, "<").replace(/>/g, ">").replace(/'/g, "'");
|
|
30
166
|
}
|
|
31
167
|
/**
|
|
32
|
-
*
|
|
168
|
+
* Renders an Inertia component and returns the appropriate HTTP response.
|
|
169
|
+
*
|
|
170
|
+
* This method handles the entire Inertia lifecycle:
|
|
171
|
+
* - Detects if the request is an Inertia AJAX request.
|
|
172
|
+
* - Validates asset versions.
|
|
173
|
+
* - Resolves props (executes functions, handles partial reloads).
|
|
174
|
+
* - Performs SSR if configured.
|
|
175
|
+
* - Wraps the result in a JSON response or the root HTML template.
|
|
33
176
|
*
|
|
34
|
-
* @param component -
|
|
35
|
-
* @param props -
|
|
36
|
-
* @param rootVars -
|
|
37
|
-
* @
|
|
177
|
+
* @param component - Frontend component name (e.g., 'Users/Profile').
|
|
178
|
+
* @param props - Data passed to the component. Can include functions for lazy evaluation.
|
|
179
|
+
* @param rootVars - Variables passed to the root HTML template (only used on initial load).
|
|
180
|
+
* @param status - Optional HTTP status code.
|
|
181
|
+
* @returns A promise that resolves to a Gravito HTTP Response.
|
|
182
|
+
*
|
|
183
|
+
* @throws {@link InertiaError}
|
|
184
|
+
* Thrown if:
|
|
185
|
+
* - The `ViewService` is missing from the context (required for initial load).
|
|
186
|
+
* - Prop serialization fails due to circular references or non-serializable data.
|
|
38
187
|
*
|
|
39
188
|
* @example
|
|
40
189
|
* ```typescript
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
190
|
+
* // Standard render
|
|
191
|
+
* return await inertia.render('Welcome', { name: 'Guest' });
|
|
192
|
+
*
|
|
193
|
+
* // Partial reload support (only 'stats' will be resolved if requested)
|
|
194
|
+
* return await inertia.render('Dashboard', {
|
|
195
|
+
* user: auth.user,
|
|
196
|
+
* stats: () => db.getStats()
|
|
197
|
+
* });
|
|
45
198
|
* ```
|
|
46
199
|
*/
|
|
47
|
-
render(component, props
|
|
48
|
-
|
|
200
|
+
async render(component, props, rootVars = {}, status) {
|
|
201
|
+
const startTime = performance.now();
|
|
202
|
+
const isInertiaRequest = Boolean(this.context.req.header("X-Inertia"));
|
|
49
203
|
try {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
204
|
+
this.log("debug", "[InertiaService] Starting render", {
|
|
205
|
+
component,
|
|
206
|
+
isInertiaRequest,
|
|
207
|
+
propsCount: props ? Object.keys(props).length : 0
|
|
208
|
+
});
|
|
209
|
+
let pageUrl;
|
|
210
|
+
try {
|
|
211
|
+
const reqUrl = new URL(this.context.req.url, "http://localhost");
|
|
212
|
+
pageUrl = reqUrl.pathname + reqUrl.search;
|
|
213
|
+
} catch {
|
|
214
|
+
pageUrl = this.context.req.url;
|
|
59
215
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
216
|
+
const resolveProps = async (p) => {
|
|
217
|
+
const partialData = this.context.req.header("X-Inertia-Partial-Data");
|
|
218
|
+
const partialExcept = this.context.req.header("X-Inertia-Partial-Except");
|
|
219
|
+
const partialComponent = this.context.req.header("X-Inertia-Partial-Component");
|
|
220
|
+
const resetKeys = this.context.req.header("X-Inertia-Reset");
|
|
221
|
+
const isPartial = partialComponent === component;
|
|
222
|
+
const only = partialData && isPartial ? partialData.split(",") : [];
|
|
223
|
+
const except = partialExcept && isPartial ? partialExcept.split(",") : [];
|
|
224
|
+
const resetKeyList = resetKeys ? resetKeys.split(",") : [];
|
|
225
|
+
const resolved = {};
|
|
226
|
+
const deferredGroups2 = {};
|
|
227
|
+
const mergedKeys2 = [];
|
|
228
|
+
for (const [key, value] of Object.entries(p)) {
|
|
229
|
+
if (only.length > 0 && !only.includes(key)) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (except.length > 0 && except.includes(key)) {
|
|
233
|
+
continue;
|
|
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
|
+
}
|
|
262
|
+
if (typeof value === "function") {
|
|
263
|
+
const propStart = performance.now();
|
|
264
|
+
try {
|
|
265
|
+
resolved[key] = await value();
|
|
266
|
+
this.log("debug", `[InertiaService] Resolved lazy prop: ${key}`, {
|
|
267
|
+
duration: `${(performance.now() - propStart).toFixed(2)}ms`
|
|
268
|
+
});
|
|
269
|
+
} catch (err) {
|
|
270
|
+
this.log("error", `[InertiaService] Failed to resolve lazy prop: ${key}`, { err });
|
|
271
|
+
throw new InertiaDataError(`Failed to resolve lazy prop: ${key}`, { cause: err });
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
resolved[key] = value;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return { resolved, deferredGroups: deferredGroups2, mergedKeys: mergedKeys2 };
|
|
278
|
+
};
|
|
279
|
+
const resolveVersion = async () => {
|
|
280
|
+
if (typeof this.config.version === "function") {
|
|
281
|
+
return await this.config.version();
|
|
282
|
+
}
|
|
283
|
+
return this.config.version;
|
|
284
|
+
};
|
|
285
|
+
const version = await resolveVersion();
|
|
286
|
+
const getMergedProps = () => {
|
|
287
|
+
const propsToMerge = props ?? {};
|
|
288
|
+
if (Object.keys(propsToMerge).length === 0) {
|
|
289
|
+
return this.sharedProps;
|
|
290
|
+
}
|
|
291
|
+
return { ...this.sharedProps, ...propsToMerge };
|
|
292
|
+
};
|
|
293
|
+
const {
|
|
294
|
+
resolved: resolvedPropsData,
|
|
295
|
+
deferredGroups,
|
|
296
|
+
mergedKeys
|
|
297
|
+
} = await resolveProps(getMergedProps());
|
|
298
|
+
const page = {
|
|
299
|
+
component,
|
|
300
|
+
props: resolvedPropsData,
|
|
301
|
+
url: pageUrl,
|
|
302
|
+
version
|
|
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
|
+
}
|
|
319
|
+
let pageJson;
|
|
320
|
+
try {
|
|
321
|
+
pageJson = JSON.stringify(page);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
throw new InertiaDataError(
|
|
324
|
+
`Inertia page serialization failed for component: ${component}`,
|
|
325
|
+
{
|
|
326
|
+
cause: error
|
|
327
|
+
}
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
let response;
|
|
331
|
+
if (isInertiaRequest) {
|
|
332
|
+
const clientVersion = this.context.req.header("X-Inertia-Version");
|
|
333
|
+
if (version && clientVersion && clientVersion !== version) {
|
|
334
|
+
this.context.header("X-Inertia-Location", pageUrl);
|
|
335
|
+
return new Response("", { status: 409 });
|
|
336
|
+
}
|
|
337
|
+
this.context.header("X-Inertia", "true");
|
|
338
|
+
this.context.header("Vary", "Accept");
|
|
339
|
+
response = this.context.json(page, status);
|
|
340
|
+
} else {
|
|
341
|
+
const view = this.context.get("view");
|
|
342
|
+
const rootView = this.config.rootView ?? "app";
|
|
343
|
+
if (!view) {
|
|
344
|
+
throw new InertiaConfigError(
|
|
345
|
+
"ViewService (OrbitPrism) is required for initial page load but was not found in context.",
|
|
346
|
+
{ hint: "Ensure OrbitPrism is loaded before OrbitIon in your orbit configuration" }
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
350
|
+
let ssrData = { head: [], body: "" };
|
|
351
|
+
if (this.config.ssr?.enabled && this.config.ssr.render) {
|
|
352
|
+
try {
|
|
353
|
+
ssrData = await this.config.ssr.render(page);
|
|
354
|
+
} catch (error) {
|
|
355
|
+
this.log("error", "[InertiaService] SSR rendering failed", { error });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
response = this.context.html(
|
|
359
|
+
view.render(
|
|
360
|
+
rootView,
|
|
361
|
+
{
|
|
362
|
+
...rootVars,
|
|
363
|
+
page: this.escapeForSingleQuotedHtmlAttribute(pageJson),
|
|
364
|
+
isDev,
|
|
365
|
+
ssrHead: ssrData.head.join("\n"),
|
|
366
|
+
ssrBody: ssrData.body
|
|
367
|
+
},
|
|
368
|
+
{ layout: "" }
|
|
369
|
+
),
|
|
370
|
+
status
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
const duration = performance.now() - startTime;
|
|
374
|
+
this.log("info", "[InertiaService] Render complete", {
|
|
375
|
+
component,
|
|
376
|
+
duration: `${duration.toFixed(2)}ms`,
|
|
377
|
+
isInertiaRequest,
|
|
378
|
+
status: status ?? 200
|
|
379
|
+
});
|
|
380
|
+
if (this.onRenderCallback) {
|
|
381
|
+
this.onRenderCallback({
|
|
382
|
+
component,
|
|
383
|
+
duration,
|
|
384
|
+
isInertiaRequest,
|
|
385
|
+
propsCount: props ? Object.keys(props).length : 0,
|
|
386
|
+
timestamp: Date.now(),
|
|
387
|
+
status
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
return response;
|
|
391
|
+
} catch (error) {
|
|
392
|
+
const duration = performance.now() - startTime;
|
|
393
|
+
if (error instanceof InertiaError) {
|
|
394
|
+
this.log("error", "[InertiaService] Render failed", {
|
|
395
|
+
component,
|
|
396
|
+
duration: `${duration.toFixed(2)}ms`,
|
|
397
|
+
errorCode: error.code,
|
|
398
|
+
errorDetails: error.details
|
|
399
|
+
});
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
this.log("error", "[InertiaService] Unexpected render error", {
|
|
403
|
+
component,
|
|
404
|
+
duration: `${duration.toFixed(2)}ms`,
|
|
405
|
+
error
|
|
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
|
+
}
|
|
443
|
+
return new Response("Inertia Render Error", { status: 500 });
|
|
77
444
|
}
|
|
78
|
-
const isDev = process.env.NODE_ENV !== "production";
|
|
79
|
-
return this.context.html(
|
|
80
|
-
view.render(
|
|
81
|
-
rootView,
|
|
82
|
-
{
|
|
83
|
-
...rootVars,
|
|
84
|
-
page: this.escapeForSingleQuotedHtmlAttribute(JSON.stringify(page)),
|
|
85
|
-
isDev
|
|
86
|
-
},
|
|
87
|
-
{ layout: "" }
|
|
88
|
-
)
|
|
89
|
-
);
|
|
90
445
|
}
|
|
91
|
-
// ...
|
|
92
446
|
/**
|
|
93
|
-
*
|
|
447
|
+
* Registers a piece of data to be shared with all Inertia responses for the current request.
|
|
94
448
|
*
|
|
95
|
-
* Shared props are merged with component-specific props
|
|
449
|
+
* Shared props are automatically merged with component-specific props during the render phase.
|
|
450
|
+
* This is the primary mechanism for providing global state like authentication details,
|
|
451
|
+
* flash messages, or configuration to the frontend.
|
|
96
452
|
*
|
|
97
|
-
* @param key -
|
|
98
|
-
* @param value -
|
|
453
|
+
* @param key - Identifier for the shared prop.
|
|
454
|
+
* @param value - Value to share. Must be JSON serializable.
|
|
99
455
|
*
|
|
100
456
|
* @example
|
|
101
457
|
* ```typescript
|
|
102
|
-
*
|
|
103
|
-
* inertia.share('auth', { user: ctx.get('
|
|
104
|
-
* inertia.share('flash', ctx.get('session')?.getFlash('message'))
|
|
458
|
+
* inertia.share('appName', 'Gravito Store');
|
|
459
|
+
* inertia.share('auth', { user: ctx.get('user') });
|
|
105
460
|
* ```
|
|
106
461
|
*/
|
|
107
462
|
share(key, value) {
|
|
108
463
|
this.sharedProps[key] = value;
|
|
109
464
|
}
|
|
110
465
|
/**
|
|
111
|
-
*
|
|
466
|
+
* Shares multiple props in a single operation.
|
|
467
|
+
*
|
|
468
|
+
* Merges the provided object into the existing shared props state. Existing keys
|
|
469
|
+
* with the same name will be overwritten.
|
|
112
470
|
*
|
|
113
|
-
* @param props - Object
|
|
471
|
+
* @param props - Object containing key-value pairs to merge into the shared state.
|
|
472
|
+
*
|
|
473
|
+
* @example
|
|
474
|
+
* ```typescript
|
|
475
|
+
* inertia.shareAll({
|
|
476
|
+
* version: '1.2.0',
|
|
477
|
+
* environment: 'production',
|
|
478
|
+
* features: ['chat', 'search']
|
|
479
|
+
* });
|
|
480
|
+
* ```
|
|
114
481
|
*/
|
|
115
482
|
shareAll(props) {
|
|
116
|
-
|
|
483
|
+
this.sharedProps = { ...this.sharedProps, ...props };
|
|
117
484
|
}
|
|
118
485
|
/**
|
|
119
|
-
*
|
|
486
|
+
* Returns a shallow copy of the current shared props.
|
|
487
|
+
*
|
|
488
|
+
* Useful for inspecting the shared state or for manual prop merging in advanced scenarios.
|
|
489
|
+
*
|
|
490
|
+
* @returns A dictionary containing all currently registered shared props.
|
|
120
491
|
*
|
|
121
|
-
* @
|
|
492
|
+
* @example
|
|
493
|
+
* ```typescript
|
|
494
|
+
* const shared = inertia.getSharedProps();
|
|
495
|
+
* if (!shared.auth) {
|
|
496
|
+
* console.warn('Authentication data is missing from shared props');
|
|
497
|
+
* }
|
|
498
|
+
* ```
|
|
122
499
|
*/
|
|
123
500
|
getSharedProps() {
|
|
124
501
|
return { ...this.sharedProps };
|
|
125
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
|
+
}
|
|
126
591
|
};
|
|
127
592
|
|
|
128
593
|
// src/index.ts
|
|
129
594
|
var OrbitIon = class {
|
|
595
|
+
/**
|
|
596
|
+
* Initializes the Orbit with custom configuration.
|
|
597
|
+
*
|
|
598
|
+
* @param options - Configuration overrides for the Inertia service.
|
|
599
|
+
*/
|
|
130
600
|
constructor(options = {}) {
|
|
131
601
|
this.options = options;
|
|
132
602
|
}
|
|
133
603
|
/**
|
|
134
|
-
*
|
|
604
|
+
* Registers the Inertia middleware and service factory into PlanetCore.
|
|
605
|
+
*
|
|
606
|
+
* This method is called automatically by Gravito during the boot process.
|
|
607
|
+
* It sets up the `InertiaService` for each request and attaches
|
|
608
|
+
* the `InertiaHelper` proxy to the context.
|
|
609
|
+
*
|
|
610
|
+
* @param core - The Gravito micro-kernel instance.
|
|
611
|
+
*
|
|
612
|
+
* @example
|
|
613
|
+
* ```typescript
|
|
614
|
+
* const ion = new OrbitIon({ version: '1.0' });
|
|
615
|
+
* ion.install(core);
|
|
616
|
+
* ```
|
|
135
617
|
*/
|
|
136
618
|
install(core) {
|
|
137
619
|
core.logger.info("\u{1F6F0}\uFE0F Orbit Inertia installed (Callable Interface)");
|
|
138
620
|
const appVersion = this.options.version ?? core.config.get("APP_VERSION", "1.0.0");
|
|
139
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
|
+
}
|
|
140
648
|
core.adapter.use("*", async (c, next) => {
|
|
141
649
|
const service = new InertiaService(c, {
|
|
142
|
-
version:
|
|
143
|
-
rootView
|
|
650
|
+
version: resolveAppVersion,
|
|
651
|
+
rootView,
|
|
652
|
+
ssr: this.options.ssr
|
|
144
653
|
});
|
|
145
|
-
const inertiaProxy = (component, props = {}, rootVars = {}) => {
|
|
146
|
-
return service.render(component, props, rootVars);
|
|
654
|
+
const inertiaProxy = async (component, props = {}, rootVars = {}, status) => {
|
|
655
|
+
return await service.render(component, props, rootVars, status);
|
|
147
656
|
};
|
|
148
657
|
Object.assign(inertiaProxy, {
|
|
149
658
|
share: service.share.bind(service),
|
|
150
659
|
shareAll: service.shareAll.bind(service),
|
|
151
660
|
getSharedProps: service.getSharedProps.bind(service),
|
|
152
661
|
render: service.render.bind(service),
|
|
153
|
-
|
|
662
|
+
location: service.location.bind(service),
|
|
663
|
+
encryptHistory: service.encryptHistory.bind(service),
|
|
664
|
+
clearHistory: service.clearHistory.bind(service),
|
|
665
|
+
withErrors: service.withErrors.bind(service),
|
|
154
666
|
service
|
|
155
|
-
// Access to the raw service instance
|
|
156
667
|
});
|
|
157
668
|
c.set("inertia", inertiaProxy);
|
|
158
669
|
return await next();
|
|
@@ -161,7 +672,11 @@ var OrbitIon = class {
|
|
|
161
672
|
};
|
|
162
673
|
var index_default = OrbitIon;
|
|
163
674
|
export {
|
|
675
|
+
InertiaConfigError,
|
|
676
|
+
InertiaDataError,
|
|
677
|
+
InertiaError,
|
|
164
678
|
InertiaService,
|
|
679
|
+
InertiaTemplateError,
|
|
165
680
|
OrbitIon,
|
|
166
681
|
index_default as default
|
|
167
682
|
};
|