@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.cjs
CHANGED
|
@@ -20,167 +20,682 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
InertiaConfigError: () => InertiaConfigError,
|
|
24
|
+
InertiaDataError: () => InertiaDataError,
|
|
25
|
+
InertiaError: () => InertiaError,
|
|
23
26
|
InertiaService: () => InertiaService,
|
|
27
|
+
InertiaTemplateError: () => InertiaTemplateError,
|
|
24
28
|
OrbitIon: () => OrbitIon,
|
|
25
29
|
default: () => index_default
|
|
26
30
|
});
|
|
27
31
|
module.exports = __toCommonJS(index_exports);
|
|
28
32
|
|
|
33
|
+
// src/errors.ts
|
|
34
|
+
var InertiaError = class extends Error {
|
|
35
|
+
constructor(code, httpStatus = 500, details) {
|
|
36
|
+
super(code);
|
|
37
|
+
this.code = code;
|
|
38
|
+
this.httpStatus = httpStatus;
|
|
39
|
+
this.details = details;
|
|
40
|
+
this.name = "InertiaError";
|
|
41
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
42
|
+
Error.captureStackTrace?.(this, this.constructor);
|
|
43
|
+
}
|
|
44
|
+
toJSON() {
|
|
45
|
+
return {
|
|
46
|
+
name: this.name,
|
|
47
|
+
code: this.code,
|
|
48
|
+
httpStatus: this.httpStatus,
|
|
49
|
+
message: this.message,
|
|
50
|
+
details: this.details
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var InertiaConfigError = class extends InertiaError {
|
|
55
|
+
constructor(message, details) {
|
|
56
|
+
super("CONFIG_ERROR", 500, {
|
|
57
|
+
...details,
|
|
58
|
+
message,
|
|
59
|
+
hint: details?.hint ?? "Check your OrbitIon configuration and ensure dependencies are loaded."
|
|
60
|
+
});
|
|
61
|
+
this.name = "InertiaConfigError";
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
var InertiaDataError = class extends InertiaError {
|
|
65
|
+
constructor(message, details) {
|
|
66
|
+
super("DATA_ERROR", 500, {
|
|
67
|
+
...details,
|
|
68
|
+
message,
|
|
69
|
+
hint: details?.hint ?? "Ensure all props are JSON-serializable (no circular refs, BigInt, or functions)."
|
|
70
|
+
});
|
|
71
|
+
this.name = "InertiaDataError";
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
var InertiaTemplateError = class extends InertiaError {
|
|
75
|
+
constructor(message, details) {
|
|
76
|
+
super("TEMPLATE_ERROR", 500, {
|
|
77
|
+
...details,
|
|
78
|
+
message,
|
|
79
|
+
hint: details?.hint ?? "Check if the root view template exists and is properly configured."
|
|
80
|
+
});
|
|
81
|
+
this.name = "InertiaTemplateError";
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
29
85
|
// src/InertiaService.ts
|
|
30
86
|
var InertiaService = class {
|
|
31
87
|
/**
|
|
32
|
-
*
|
|
88
|
+
* Initializes a new instance of the Inertia service.
|
|
33
89
|
*
|
|
34
|
-
* @param context - The Gravito request context
|
|
35
|
-
* @param config -
|
|
90
|
+
* @param context - The current Gravito request context.
|
|
91
|
+
* @param config - Instance configuration options.
|
|
36
92
|
*/
|
|
37
93
|
constructor(context, config = {}) {
|
|
38
94
|
this.context = context;
|
|
39
95
|
this.config = config;
|
|
96
|
+
this.logLevel = config.logLevel ?? "info";
|
|
97
|
+
this.onRenderCallback = config.onRender;
|
|
40
98
|
}
|
|
41
99
|
sharedProps = {};
|
|
100
|
+
errorBags = {};
|
|
101
|
+
encryptHistoryFlag = false;
|
|
102
|
+
clearHistoryFlag = false;
|
|
103
|
+
logLevel;
|
|
104
|
+
onRenderCallback;
|
|
105
|
+
/**
|
|
106
|
+
* Creates a deferred prop that will be loaded after the initial render.
|
|
107
|
+
*
|
|
108
|
+
* @param factory - Function that resolves to the prop value.
|
|
109
|
+
* @param group - Optional group name for organizing deferred props.
|
|
110
|
+
* @returns A deferred prop definition.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```typescript
|
|
114
|
+
* props: {
|
|
115
|
+
* user: { id: 1 },
|
|
116
|
+
* stats: InertiaService.defer(() => fetchStats())
|
|
117
|
+
* }
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
static defer(factory, group = "default") {
|
|
121
|
+
return {
|
|
122
|
+
_type: "deferred",
|
|
123
|
+
factory,
|
|
124
|
+
group
|
|
125
|
+
};
|
|
126
|
+
}
|
|
42
127
|
/**
|
|
43
|
-
*
|
|
128
|
+
* Marks a prop to be merged (shallow) with existing data during partial reloads.
|
|
44
129
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
130
|
+
* @param value - The prop value to merge.
|
|
131
|
+
* @param matchOn - Optional key(s) to match on when merging arrays.
|
|
132
|
+
* @returns A merge prop definition.
|
|
133
|
+
*/
|
|
134
|
+
static merge(value, matchOn) {
|
|
135
|
+
return {
|
|
136
|
+
_type: "merge",
|
|
137
|
+
value,
|
|
138
|
+
matchOn
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Marks a prop to prepend to an array during partial reloads.
|
|
48
143
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
|
|
144
|
+
* @param value - The prop value to prepend.
|
|
145
|
+
* @returns A prepend prop definition.
|
|
146
|
+
*/
|
|
147
|
+
static prepend(value) {
|
|
148
|
+
return {
|
|
149
|
+
_type: "prepend",
|
|
150
|
+
value
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Marks a prop to be deep-merged with existing data during partial reloads.
|
|
52
155
|
*
|
|
53
|
-
* @param value - The
|
|
54
|
-
* @returns
|
|
156
|
+
* @param value - The prop value to deep merge.
|
|
157
|
+
* @returns A deep merge prop definition.
|
|
158
|
+
*/
|
|
159
|
+
static deepMerge(value) {
|
|
160
|
+
return {
|
|
161
|
+
_type: "deepMerge",
|
|
162
|
+
value
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Internal logging helper that respects the configured log level.
|
|
167
|
+
*
|
|
168
|
+
* Routes logs to the Gravito context logger if available.
|
|
169
|
+
*
|
|
170
|
+
* @param level - Log severity level.
|
|
171
|
+
* @param message - Descriptive message.
|
|
172
|
+
* @param data - Optional metadata for the log.
|
|
173
|
+
*/
|
|
174
|
+
log(level, message, data) {
|
|
175
|
+
const levels = ["debug", "info", "warn", "error", "silent"];
|
|
176
|
+
const currentLevelIndex = levels.indexOf(this.logLevel);
|
|
177
|
+
const messageLevelIndex = levels.indexOf(level);
|
|
178
|
+
if (this.logLevel === "silent" || messageLevelIndex < currentLevelIndex) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const logger = typeof this.context.get === "function" ? this.context.get("logger") : void 0;
|
|
182
|
+
if (logger && typeof logger[level] === "function") {
|
|
183
|
+
logger[level](message, data);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Escapes a string for safe embedding into a single-quoted HTML attribute.
|
|
188
|
+
*
|
|
189
|
+
* This ensures that JSON strings can be safely passed to the frontend
|
|
190
|
+
* via the `data-page` attribute without breaking the HTML structure or
|
|
191
|
+
* introducing XSS vulnerabilities.
|
|
192
|
+
*
|
|
193
|
+
* @param value - Raw JSON or text string.
|
|
194
|
+
* @returns Safely escaped HTML attribute value.
|
|
55
195
|
*/
|
|
56
196
|
escapeForSingleQuotedHtmlAttribute(value) {
|
|
57
197
|
return value.replace(/&/g, "&").replace(/\\"/g, "\\"").replace(/</g, "<").replace(/>/g, ">").replace(/'/g, "'");
|
|
58
198
|
}
|
|
59
199
|
/**
|
|
60
|
-
*
|
|
200
|
+
* Renders an Inertia component and returns the appropriate HTTP response.
|
|
201
|
+
*
|
|
202
|
+
* This method handles the entire Inertia lifecycle:
|
|
203
|
+
* - Detects if the request is an Inertia AJAX request.
|
|
204
|
+
* - Validates asset versions.
|
|
205
|
+
* - Resolves props (executes functions, handles partial reloads).
|
|
206
|
+
* - Performs SSR if configured.
|
|
207
|
+
* - Wraps the result in a JSON response or the root HTML template.
|
|
61
208
|
*
|
|
62
|
-
* @param component -
|
|
63
|
-
* @param props -
|
|
64
|
-
* @param rootVars -
|
|
65
|
-
* @
|
|
209
|
+
* @param component - Frontend component name (e.g., 'Users/Profile').
|
|
210
|
+
* @param props - Data passed to the component. Can include functions for lazy evaluation.
|
|
211
|
+
* @param rootVars - Variables passed to the root HTML template (only used on initial load).
|
|
212
|
+
* @param status - Optional HTTP status code.
|
|
213
|
+
* @returns A promise that resolves to a Gravito HTTP Response.
|
|
214
|
+
*
|
|
215
|
+
* @throws {@link InertiaError}
|
|
216
|
+
* Thrown if:
|
|
217
|
+
* - The `ViewService` is missing from the context (required for initial load).
|
|
218
|
+
* - Prop serialization fails due to circular references or non-serializable data.
|
|
66
219
|
*
|
|
67
220
|
* @example
|
|
68
221
|
* ```typescript
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
222
|
+
* // Standard render
|
|
223
|
+
* return await inertia.render('Welcome', { name: 'Guest' });
|
|
224
|
+
*
|
|
225
|
+
* // Partial reload support (only 'stats' will be resolved if requested)
|
|
226
|
+
* return await inertia.render('Dashboard', {
|
|
227
|
+
* user: auth.user,
|
|
228
|
+
* stats: () => db.getStats()
|
|
229
|
+
* });
|
|
73
230
|
* ```
|
|
74
231
|
*/
|
|
75
|
-
render(component, props
|
|
76
|
-
|
|
232
|
+
async render(component, props, rootVars = {}, status) {
|
|
233
|
+
const startTime = performance.now();
|
|
234
|
+
const isInertiaRequest = Boolean(this.context.req.header("X-Inertia"));
|
|
77
235
|
try {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
236
|
+
this.log("debug", "[InertiaService] Starting render", {
|
|
237
|
+
component,
|
|
238
|
+
isInertiaRequest,
|
|
239
|
+
propsCount: props ? Object.keys(props).length : 0
|
|
240
|
+
});
|
|
241
|
+
let pageUrl;
|
|
242
|
+
try {
|
|
243
|
+
const reqUrl = new URL(this.context.req.url, "http://localhost");
|
|
244
|
+
pageUrl = reqUrl.pathname + reqUrl.search;
|
|
245
|
+
} catch {
|
|
246
|
+
pageUrl = this.context.req.url;
|
|
87
247
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
248
|
+
const resolveProps = async (p) => {
|
|
249
|
+
const partialData = this.context.req.header("X-Inertia-Partial-Data");
|
|
250
|
+
const partialExcept = this.context.req.header("X-Inertia-Partial-Except");
|
|
251
|
+
const partialComponent = this.context.req.header("X-Inertia-Partial-Component");
|
|
252
|
+
const resetKeys = this.context.req.header("X-Inertia-Reset");
|
|
253
|
+
const isPartial = partialComponent === component;
|
|
254
|
+
const only = partialData && isPartial ? partialData.split(",") : [];
|
|
255
|
+
const except = partialExcept && isPartial ? partialExcept.split(",") : [];
|
|
256
|
+
const resetKeyList = resetKeys ? resetKeys.split(",") : [];
|
|
257
|
+
const resolved = {};
|
|
258
|
+
const deferredGroups2 = {};
|
|
259
|
+
const mergedKeys2 = [];
|
|
260
|
+
for (const [key, value] of Object.entries(p)) {
|
|
261
|
+
if (only.length > 0 && !only.includes(key)) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (except.length > 0 && except.includes(key)) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (value && typeof value === "object" && "_type" in value && value._type === "deferred") {
|
|
268
|
+
const deferred = value;
|
|
269
|
+
const group = deferred.group ?? "default";
|
|
270
|
+
if (!deferredGroups2[group]) {
|
|
271
|
+
deferredGroups2[group] = [];
|
|
272
|
+
}
|
|
273
|
+
deferredGroups2[group].push(key);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (value && typeof value === "object" && "_type" in value && ["merge", "prepend", "deepMerge"].includes(value._type)) {
|
|
277
|
+
const merged = value;
|
|
278
|
+
const existing = mergedKeys2.find((m) => m.mode === merged._type);
|
|
279
|
+
if (existing) {
|
|
280
|
+
existing.keys.push(key);
|
|
281
|
+
} else {
|
|
282
|
+
mergedKeys2.push({
|
|
283
|
+
keys: [key],
|
|
284
|
+
mode: merged._type
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
resolved[key] = merged.value;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (resetKeyList.includes(key)) {
|
|
291
|
+
resolved[key] = void 0;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (typeof value === "function") {
|
|
295
|
+
const propStart = performance.now();
|
|
296
|
+
try {
|
|
297
|
+
resolved[key] = await value();
|
|
298
|
+
this.log("debug", `[InertiaService] Resolved lazy prop: ${key}`, {
|
|
299
|
+
duration: `${(performance.now() - propStart).toFixed(2)}ms`
|
|
300
|
+
});
|
|
301
|
+
} catch (err) {
|
|
302
|
+
this.log("error", `[InertiaService] Failed to resolve lazy prop: ${key}`, { err });
|
|
303
|
+
throw new InertiaDataError(`Failed to resolve lazy prop: ${key}`, { cause: err });
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
resolved[key] = value;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return { resolved, deferredGroups: deferredGroups2, mergedKeys: mergedKeys2 };
|
|
310
|
+
};
|
|
311
|
+
const resolveVersion = async () => {
|
|
312
|
+
if (typeof this.config.version === "function") {
|
|
313
|
+
return await this.config.version();
|
|
314
|
+
}
|
|
315
|
+
return this.config.version;
|
|
316
|
+
};
|
|
317
|
+
const version = await resolveVersion();
|
|
318
|
+
const getMergedProps = () => {
|
|
319
|
+
const propsToMerge = props ?? {};
|
|
320
|
+
if (Object.keys(propsToMerge).length === 0) {
|
|
321
|
+
return this.sharedProps;
|
|
322
|
+
}
|
|
323
|
+
return { ...this.sharedProps, ...propsToMerge };
|
|
324
|
+
};
|
|
325
|
+
const {
|
|
326
|
+
resolved: resolvedPropsData,
|
|
327
|
+
deferredGroups,
|
|
328
|
+
mergedKeys
|
|
329
|
+
} = await resolveProps(getMergedProps());
|
|
330
|
+
const page = {
|
|
331
|
+
component,
|
|
332
|
+
props: resolvedPropsData,
|
|
333
|
+
url: pageUrl,
|
|
334
|
+
version
|
|
335
|
+
};
|
|
336
|
+
if (Object.keys(deferredGroups).length > 0) {
|
|
337
|
+
page.deferredProps = deferredGroups;
|
|
338
|
+
}
|
|
339
|
+
if (mergedKeys.length > 0) {
|
|
340
|
+
page.mergeProps = mergedKeys;
|
|
341
|
+
}
|
|
342
|
+
if (this.encryptHistoryFlag) {
|
|
343
|
+
page.encryptHistory = true;
|
|
344
|
+
}
|
|
345
|
+
if (this.clearHistoryFlag) {
|
|
346
|
+
page.clearHistory = true;
|
|
347
|
+
}
|
|
348
|
+
if (Object.keys(this.errorBags).length > 0) {
|
|
349
|
+
page.errorBags = this.errorBags;
|
|
350
|
+
}
|
|
351
|
+
let pageJson;
|
|
352
|
+
try {
|
|
353
|
+
pageJson = JSON.stringify(page);
|
|
354
|
+
} catch (error) {
|
|
355
|
+
throw new InertiaDataError(
|
|
356
|
+
`Inertia page serialization failed for component: ${component}`,
|
|
357
|
+
{
|
|
358
|
+
cause: error
|
|
359
|
+
}
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
let response;
|
|
363
|
+
if (isInertiaRequest) {
|
|
364
|
+
const clientVersion = this.context.req.header("X-Inertia-Version");
|
|
365
|
+
if (version && clientVersion && clientVersion !== version) {
|
|
366
|
+
this.context.header("X-Inertia-Location", pageUrl);
|
|
367
|
+
return new Response("", { status: 409 });
|
|
368
|
+
}
|
|
369
|
+
this.context.header("X-Inertia", "true");
|
|
370
|
+
this.context.header("Vary", "Accept");
|
|
371
|
+
response = this.context.json(page, status);
|
|
372
|
+
} else {
|
|
373
|
+
const view = this.context.get("view");
|
|
374
|
+
const rootView = this.config.rootView ?? "app";
|
|
375
|
+
if (!view) {
|
|
376
|
+
throw new InertiaConfigError(
|
|
377
|
+
"ViewService (OrbitPrism) is required for initial page load but was not found in context.",
|
|
378
|
+
{ hint: "Ensure OrbitPrism is loaded before OrbitIon in your orbit configuration" }
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
382
|
+
let ssrData = { head: [], body: "" };
|
|
383
|
+
if (this.config.ssr?.enabled && this.config.ssr.render) {
|
|
384
|
+
try {
|
|
385
|
+
ssrData = await this.config.ssr.render(page);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
this.log("error", "[InertiaService] SSR rendering failed", { error });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
response = this.context.html(
|
|
391
|
+
view.render(
|
|
392
|
+
rootView,
|
|
393
|
+
{
|
|
394
|
+
...rootVars,
|
|
395
|
+
page: this.escapeForSingleQuotedHtmlAttribute(pageJson),
|
|
396
|
+
isDev,
|
|
397
|
+
ssrHead: ssrData.head.join("\n"),
|
|
398
|
+
ssrBody: ssrData.body
|
|
399
|
+
},
|
|
400
|
+
{ layout: "" }
|
|
401
|
+
),
|
|
402
|
+
status
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
const duration = performance.now() - startTime;
|
|
406
|
+
this.log("info", "[InertiaService] Render complete", {
|
|
407
|
+
component,
|
|
408
|
+
duration: `${duration.toFixed(2)}ms`,
|
|
409
|
+
isInertiaRequest,
|
|
410
|
+
status: status ?? 200
|
|
411
|
+
});
|
|
412
|
+
if (this.onRenderCallback) {
|
|
413
|
+
this.onRenderCallback({
|
|
414
|
+
component,
|
|
415
|
+
duration,
|
|
416
|
+
isInertiaRequest,
|
|
417
|
+
propsCount: props ? Object.keys(props).length : 0,
|
|
418
|
+
timestamp: Date.now(),
|
|
419
|
+
status
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
return response;
|
|
423
|
+
} catch (error) {
|
|
424
|
+
const duration = performance.now() - startTime;
|
|
425
|
+
if (error instanceof InertiaError) {
|
|
426
|
+
this.log("error", "[InertiaService] Render failed", {
|
|
427
|
+
component,
|
|
428
|
+
duration: `${duration.toFixed(2)}ms`,
|
|
429
|
+
errorCode: error.code,
|
|
430
|
+
errorDetails: error.details
|
|
431
|
+
});
|
|
432
|
+
throw error;
|
|
433
|
+
}
|
|
434
|
+
this.log("error", "[InertiaService] Unexpected render error", {
|
|
435
|
+
component,
|
|
436
|
+
duration: `${duration.toFixed(2)}ms`,
|
|
437
|
+
error
|
|
438
|
+
});
|
|
439
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
440
|
+
if (isDev && error instanceof Error) {
|
|
441
|
+
const errorHtml = `
|
|
442
|
+
<!DOCTYPE html>
|
|
443
|
+
<html>
|
|
444
|
+
<head>
|
|
445
|
+
<meta charset="utf-8">
|
|
446
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
447
|
+
<title>Inertia Render Error</title>
|
|
448
|
+
<style>
|
|
449
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
450
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto; background: #f5f5f5; padding: 20px; }
|
|
451
|
+
.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); }
|
|
452
|
+
h1 { color: #d32f2f; margin-bottom: 10px; font-size: 24px; }
|
|
453
|
+
.meta { color: #666; margin-bottom: 20px; font-size: 14px; }
|
|
454
|
+
.component { background: #f5f5f5; padding: 10px 15px; border-left: 3px solid #1976d2; margin-bottom: 20px; font-family: monospace; }
|
|
455
|
+
.error-message { background: #ffebee; border: 1px solid #ef5350; border-radius: 4px; padding: 15px; margin-bottom: 20px; color: #c62828; }
|
|
456
|
+
.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; }
|
|
457
|
+
.hint { background: #e8f5e9; border-left: 3px solid #4caf50; padding: 15px; margin-top: 20px; color: #2e7d32; }
|
|
458
|
+
</style>
|
|
459
|
+
</head>
|
|
460
|
+
<body>
|
|
461
|
+
<div class="container">
|
|
462
|
+
<h1>\u{1F6A8} Inertia Render Error</h1>
|
|
463
|
+
<div class="meta">Component: <span class="component">${component}</span></div>
|
|
464
|
+
<div class="error-message">${error.message || "Unknown error"}</div>
|
|
465
|
+
${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>` : ""}
|
|
466
|
+
<div class="hint">
|
|
467
|
+
<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.
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
</body>
|
|
471
|
+
</html>
|
|
472
|
+
`;
|
|
473
|
+
return this.context.html(errorHtml, 500);
|
|
474
|
+
}
|
|
475
|
+
return new Response("Inertia Render Error", { status: 500 });
|
|
105
476
|
}
|
|
106
|
-
const isDev = process.env.NODE_ENV !== "production";
|
|
107
|
-
return this.context.html(
|
|
108
|
-
view.render(
|
|
109
|
-
rootView,
|
|
110
|
-
{
|
|
111
|
-
...rootVars,
|
|
112
|
-
page: this.escapeForSingleQuotedHtmlAttribute(JSON.stringify(page)),
|
|
113
|
-
isDev
|
|
114
|
-
},
|
|
115
|
-
{ layout: "" }
|
|
116
|
-
)
|
|
117
|
-
);
|
|
118
477
|
}
|
|
119
|
-
// ...
|
|
120
478
|
/**
|
|
121
|
-
*
|
|
479
|
+
* Registers a piece of data to be shared with all Inertia responses for the current request.
|
|
122
480
|
*
|
|
123
|
-
* Shared props are merged with component-specific props
|
|
481
|
+
* Shared props are automatically merged with component-specific props during the render phase.
|
|
482
|
+
* This is the primary mechanism for providing global state like authentication details,
|
|
483
|
+
* flash messages, or configuration to the frontend.
|
|
124
484
|
*
|
|
125
|
-
* @param key -
|
|
126
|
-
* @param value -
|
|
485
|
+
* @param key - Identifier for the shared prop.
|
|
486
|
+
* @param value - Value to share. Must be JSON serializable.
|
|
127
487
|
*
|
|
128
488
|
* @example
|
|
129
489
|
* ```typescript
|
|
130
|
-
*
|
|
131
|
-
* inertia.share('auth', { user: ctx.get('
|
|
132
|
-
* inertia.share('flash', ctx.get('session')?.getFlash('message'))
|
|
490
|
+
* inertia.share('appName', 'Gravito Store');
|
|
491
|
+
* inertia.share('auth', { user: ctx.get('user') });
|
|
133
492
|
* ```
|
|
134
493
|
*/
|
|
135
494
|
share(key, value) {
|
|
136
495
|
this.sharedProps[key] = value;
|
|
137
496
|
}
|
|
138
497
|
/**
|
|
139
|
-
*
|
|
498
|
+
* Shares multiple props in a single operation.
|
|
499
|
+
*
|
|
500
|
+
* Merges the provided object into the existing shared props state. Existing keys
|
|
501
|
+
* with the same name will be overwritten.
|
|
140
502
|
*
|
|
141
|
-
* @param props - Object
|
|
503
|
+
* @param props - Object containing key-value pairs to merge into the shared state.
|
|
504
|
+
*
|
|
505
|
+
* @example
|
|
506
|
+
* ```typescript
|
|
507
|
+
* inertia.shareAll({
|
|
508
|
+
* version: '1.2.0',
|
|
509
|
+
* environment: 'production',
|
|
510
|
+
* features: ['chat', 'search']
|
|
511
|
+
* });
|
|
512
|
+
* ```
|
|
142
513
|
*/
|
|
143
514
|
shareAll(props) {
|
|
144
|
-
|
|
515
|
+
this.sharedProps = { ...this.sharedProps, ...props };
|
|
145
516
|
}
|
|
146
517
|
/**
|
|
147
|
-
*
|
|
518
|
+
* Returns a shallow copy of the current shared props.
|
|
519
|
+
*
|
|
520
|
+
* Useful for inspecting the shared state or for manual prop merging in advanced scenarios.
|
|
521
|
+
*
|
|
522
|
+
* @returns A dictionary containing all currently registered shared props.
|
|
148
523
|
*
|
|
149
|
-
* @
|
|
524
|
+
* @example
|
|
525
|
+
* ```typescript
|
|
526
|
+
* const shared = inertia.getSharedProps();
|
|
527
|
+
* if (!shared.auth) {
|
|
528
|
+
* console.warn('Authentication data is missing from shared props');
|
|
529
|
+
* }
|
|
530
|
+
* ```
|
|
150
531
|
*/
|
|
151
532
|
getSharedProps() {
|
|
152
533
|
return { ...this.sharedProps };
|
|
153
534
|
}
|
|
535
|
+
/**
|
|
536
|
+
* Instructs the Inertia client to navigate to a different URL.
|
|
537
|
+
*
|
|
538
|
+
* For Inertia AJAX requests, this returns a 409 Conflict with the `X-Inertia-Location` header.
|
|
539
|
+
* For regular page loads, this returns a 302 Found redirect.
|
|
540
|
+
*
|
|
541
|
+
* This is useful for server-side redirects triggered by authentication or authorization checks.
|
|
542
|
+
*
|
|
543
|
+
* @param url - The URL to redirect to.
|
|
544
|
+
* @returns A redirect response.
|
|
545
|
+
*
|
|
546
|
+
* @example
|
|
547
|
+
* ```typescript
|
|
548
|
+
* if (!ctx.get('user')) {
|
|
549
|
+
* return inertia.location('/login');
|
|
550
|
+
* }
|
|
551
|
+
* ```
|
|
552
|
+
*/
|
|
553
|
+
location(url) {
|
|
554
|
+
const isInertiaRequest = Boolean(this.context.req.header("X-Inertia"));
|
|
555
|
+
if (isInertiaRequest) {
|
|
556
|
+
this.context.header("X-Inertia-Location", url);
|
|
557
|
+
return new Response("", { status: 409 });
|
|
558
|
+
}
|
|
559
|
+
this.context.header("Location", url);
|
|
560
|
+
return new Response("", { status: 302 });
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Controls whether the Inertia client should encrypt browser history.
|
|
564
|
+
*
|
|
565
|
+
* When enabled, history is not written to the browser's History API,
|
|
566
|
+
* preventing users from using the browser back button to return to previous pages.
|
|
567
|
+
*
|
|
568
|
+
* @param encrypt - Whether to encrypt history (defaults to true).
|
|
569
|
+
* @returns The service instance for method chaining.
|
|
570
|
+
*
|
|
571
|
+
* @example
|
|
572
|
+
* ```typescript
|
|
573
|
+
* return await inertia.encryptHistory(true).render('SecurePage');
|
|
574
|
+
* ```
|
|
575
|
+
*/
|
|
576
|
+
encryptHistory(encrypt = true) {
|
|
577
|
+
this.encryptHistoryFlag = encrypt;
|
|
578
|
+
return this;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Clears the browser history after the page load.
|
|
582
|
+
*
|
|
583
|
+
* Useful for sensitive operations or multi-step wizards where you don't want
|
|
584
|
+
* users navigating back to previous states.
|
|
585
|
+
*
|
|
586
|
+
* @returns The service instance for method chaining.
|
|
587
|
+
*
|
|
588
|
+
* @example
|
|
589
|
+
* ```typescript
|
|
590
|
+
* return await inertia.clearHistory().render('SuccessPage');
|
|
591
|
+
* ```
|
|
592
|
+
*/
|
|
593
|
+
clearHistory() {
|
|
594
|
+
this.clearHistoryFlag = true;
|
|
595
|
+
return this;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Registers form validation errors organized into named bags.
|
|
599
|
+
*
|
|
600
|
+
* Error bags allow multiple validation failure scenarios to coexist.
|
|
601
|
+
* For example, you might have 'default' errors and 'import' errors from different forms.
|
|
602
|
+
*
|
|
603
|
+
* @param errors - Validation errors, with field names as keys and error messages as values.
|
|
604
|
+
* @param bag - The error bag name (defaults to 'default').
|
|
605
|
+
* @returns The service instance for method chaining.
|
|
606
|
+
*
|
|
607
|
+
* @example
|
|
608
|
+
* ```typescript
|
|
609
|
+
* inertia.withErrors({
|
|
610
|
+
* email: 'Email is required',
|
|
611
|
+
* password: 'Must be 8+ characters'
|
|
612
|
+
* }, 'login');
|
|
613
|
+
*
|
|
614
|
+
* inertia.withErrors({
|
|
615
|
+
* line_1: 'Invalid CSV format'
|
|
616
|
+
* }, 'import');
|
|
617
|
+
* ```
|
|
618
|
+
*/
|
|
619
|
+
withErrors(errors, bag = "default") {
|
|
620
|
+
this.errorBags[bag] = errors;
|
|
621
|
+
return this;
|
|
622
|
+
}
|
|
154
623
|
};
|
|
155
624
|
|
|
156
625
|
// src/index.ts
|
|
157
626
|
var OrbitIon = class {
|
|
627
|
+
/**
|
|
628
|
+
* Initializes the Orbit with custom configuration.
|
|
629
|
+
*
|
|
630
|
+
* @param options - Configuration overrides for the Inertia service.
|
|
631
|
+
*/
|
|
158
632
|
constructor(options = {}) {
|
|
159
633
|
this.options = options;
|
|
160
634
|
}
|
|
161
635
|
/**
|
|
162
|
-
*
|
|
636
|
+
* Registers the Inertia middleware and service factory into PlanetCore.
|
|
637
|
+
*
|
|
638
|
+
* This method is called automatically by Gravito during the boot process.
|
|
639
|
+
* It sets up the `InertiaService` for each request and attaches
|
|
640
|
+
* the `InertiaHelper` proxy to the context.
|
|
641
|
+
*
|
|
642
|
+
* @param core - The Gravito micro-kernel instance.
|
|
643
|
+
*
|
|
644
|
+
* @example
|
|
645
|
+
* ```typescript
|
|
646
|
+
* const ion = new OrbitIon({ version: '1.0' });
|
|
647
|
+
* ion.install(core);
|
|
648
|
+
* ```
|
|
163
649
|
*/
|
|
164
650
|
install(core) {
|
|
165
651
|
core.logger.info("\u{1F6F0}\uFE0F Orbit Inertia installed (Callable Interface)");
|
|
166
652
|
const appVersion = this.options.version ?? core.config.get("APP_VERSION", "1.0.0");
|
|
167
653
|
const rootView = this.options.rootView ?? "app";
|
|
654
|
+
const csrfEnabled = this.options.csrf?.enabled !== false;
|
|
655
|
+
const csrfCookieName = this.options.csrf?.cookieName ?? "XSRF-TOKEN";
|
|
656
|
+
const VERSION_CACHE_TTL = 6e4;
|
|
657
|
+
let cachedVersion;
|
|
658
|
+
let versionCacheTime = 0;
|
|
659
|
+
const resolveAppVersion = async () => {
|
|
660
|
+
if (typeof appVersion !== "function") {
|
|
661
|
+
return appVersion;
|
|
662
|
+
}
|
|
663
|
+
const now = Date.now();
|
|
664
|
+
if (cachedVersion && now - versionCacheTime < VERSION_CACHE_TTL) {
|
|
665
|
+
return cachedVersion;
|
|
666
|
+
}
|
|
667
|
+
cachedVersion = await appVersion();
|
|
668
|
+
versionCacheTime = now;
|
|
669
|
+
return cachedVersion;
|
|
670
|
+
};
|
|
671
|
+
if (csrfEnabled) {
|
|
672
|
+
core.adapter.use("*", async (c, next) => {
|
|
673
|
+
const token = crypto.randomUUID();
|
|
674
|
+
const secure = process.env.NODE_ENV === "production";
|
|
675
|
+
const cookieValue = `${csrfCookieName}=${token}; Path=/; ${secure ? "Secure; " : ""}SameSite=Lax`;
|
|
676
|
+
c.header("Set-Cookie", cookieValue);
|
|
677
|
+
return await next();
|
|
678
|
+
});
|
|
679
|
+
}
|
|
168
680
|
core.adapter.use("*", async (c, next) => {
|
|
169
681
|
const service = new InertiaService(c, {
|
|
170
|
-
version:
|
|
171
|
-
rootView
|
|
682
|
+
version: resolveAppVersion,
|
|
683
|
+
rootView,
|
|
684
|
+
ssr: this.options.ssr
|
|
172
685
|
});
|
|
173
|
-
const inertiaProxy = (component, props = {}, rootVars = {}) => {
|
|
174
|
-
return service.render(component, props, rootVars);
|
|
686
|
+
const inertiaProxy = async (component, props = {}, rootVars = {}, status) => {
|
|
687
|
+
return await service.render(component, props, rootVars, status);
|
|
175
688
|
};
|
|
176
689
|
Object.assign(inertiaProxy, {
|
|
177
690
|
share: service.share.bind(service),
|
|
178
691
|
shareAll: service.shareAll.bind(service),
|
|
179
692
|
getSharedProps: service.getSharedProps.bind(service),
|
|
180
693
|
render: service.render.bind(service),
|
|
181
|
-
|
|
694
|
+
location: service.location.bind(service),
|
|
695
|
+
encryptHistory: service.encryptHistory.bind(service),
|
|
696
|
+
clearHistory: service.clearHistory.bind(service),
|
|
697
|
+
withErrors: service.withErrors.bind(service),
|
|
182
698
|
service
|
|
183
|
-
// Access to the raw service instance
|
|
184
699
|
});
|
|
185
700
|
c.set("inertia", inertiaProxy);
|
|
186
701
|
return await next();
|
|
@@ -190,6 +705,10 @@ var OrbitIon = class {
|
|
|
190
705
|
var index_default = OrbitIon;
|
|
191
706
|
// Annotate the CommonJS export names for ESM import in node:
|
|
192
707
|
0 && (module.exports = {
|
|
708
|
+
InertiaConfigError,
|
|
709
|
+
InertiaDataError,
|
|
710
|
+
InertiaError,
|
|
193
711
|
InertiaService,
|
|
712
|
+
InertiaTemplateError,
|
|
194
713
|
OrbitIon
|
|
195
714
|
});
|